diff options
| author | Kamil Trzciński <ayufan@ayufan.eu> | 2018-02-28 20:36:07 +0100 | 
|---|---|---|
| committer | Kamil Trzciński <ayufan@ayufan.eu> | 2018-02-28 20:36:07 +0100 | 
| commit | e3fafa7632e038927085cf8c8228c93be44b36bd (patch) | |
| tree | 4fba0d291e945415b0f0eddd40c615cd6cd70013 /app | |
| parent | e0401df1214397626e65e58166988fe62715d372 (diff) | |
| parent | f2f58a60b76acd479e37bdbc9246ec9f9b2bea82 (diff) | |
| download | gitlab-ce-e3fafa7632e038927085cf8c8228c93be44b36bd.tar.gz | |
Merge commit 'f2f58a60b76acd479e37bdbc9246ec9f9b2bea82' into object-storage-ee-to-ce-backport
Diffstat (limited to 'app')
674 files changed, 12427 insertions, 8209 deletions
diff --git a/app/assets/images/auth_buttons/signin_with_google.png b/app/assets/images/auth_buttons/signin_with_google.png Binary files differindex b1327b4f7b4..f27bb243304 100644 --- a/app/assets/images/auth_buttons/signin_with_google.png +++ b/app/assets/images/auth_buttons/signin_with_google.png diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json index c0ed2ffdcb2..d8d173612d5 100644 --- a/app/assets/images/icons.json +++ b/app/assets/images/icons.json @@ -1 +1 @@ -{"iconCount":164,"spriteSize":72823,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","dashboard","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file +{"iconCount":173,"spriteSize":75815,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg index b9829d0d450..c8f10628713 100644 --- a/app/assets/images/icons.svg +++ b/app/assets/images/icons.svg @@ -1 +1 @@ -<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 15V1a1 1 0 0 1 1-1h4.604c.93 0 1.762.088 2.495.264.733.176 1.353.445 1.863.807.509.363.897.82 1.164 1.369.268.549.401 1.197.401 1.945 0 .366-.045.718-.137 1.055-.091.337-.23.652-.417.945a3.453 3.453 0 0 1-.71.796 3.645 3.645 0 0 1-1.021.588c.469.117.87.295 1.203.533.333.238.608.515.824.83.216.315.374.657.473 1.027.099.37.148.75.148 1.138 0 1.553-.5 2.725-1.5 3.516-1 .791-2.423 1.187-4.27 1.187H3a1 1 0 0 1-1-1zm3.297-5.967v4.319H8.12c.425 0 .791-.053 1.099-.16.307-.106.564-.252.769-.44.205-.186.357-.406.456-.659.099-.252.148-.529.148-.83a3.04 3.04 0 0 0-.131-.928 1.78 1.78 0 0 0-.413-.703 1.8 1.8 0 0 0-.73-.445c-.3-.103-.66-.154-1.077-.154H5.297zm0-2.33h2.44c.842-.014 1.468-.192 1.878-.533.41-.34.616-.826.616-1.456 0-.725-.21-1.247-.632-1.566-.421-.318-1.086-.478-1.995-.478H5.297v4.033z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-additions" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 24 30" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><title>cursor_active</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#FFF" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#1F78D1"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787a6.92 6.92 0 0 0-2.558.469c-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068a.19.19 0 0 1 .033-.067.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26a2.57 2.57 0 0 0 .221-.342c.054-.103.114-.235.181-.395a4.18 4.18 0 0 0 .174-.51c-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#FFF" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.009 6.958a4 4 0 0 0 5.283 4.775 1 1 0 0 1 .712 1.87A6 6 0 0 1 2.077 6.44l-.741-.2a.5.5 0 0 1-.12-.915L3.41 4.058a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.711-1.87 6 6 0 0 1 7.927 7.162l.74.2a.5.5 0 0 1 .121.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="ehfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="ehsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="ehthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="talic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 0h7a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm2 2h3L8 14H5L8 2zM3 14h7a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg>
\ No newline at end of file +<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 24 30" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><title>cursor_active</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#FFF" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#1F78D1"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787a6.92 6.92 0 0 0-2.558.469c-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068a.19.19 0 0 1 .033-.067.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26a2.57 2.57 0 0 0 .221-.342c.054-.103.114-.235.181-.395a4.18 4.18 0 0 0 .174-.51c-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#FFF" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="eofirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="eosecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="eothird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/epics.svg b/app/assets/images/illustrations/epics.svg new file mode 100644 index 00000000000..1a37e6bba5f --- /dev/null +++ b/app/assets/images/illustrations/epics.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="430" height="300" viewBox="0 0 430 300"><g fill="none" fill-rule="evenodd"><g transform="translate(75 53)"><rect width="284" height="208" y="5" fill="#F9F9F9" rx="10"/><rect width="284" height="208" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v188a6 6 0 0 0 6 6h264a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h264c5.523 0 10 4.477 10 10v188c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0z"/><path fill="#EEE" fill-rule="nonzero" d="M25.168 153.995c3.837-.215 7.173.028 10.119.691a3 3 0 1 0 1.318-5.853c-3.509-.79-7.4-1.074-11.773-.828a3 3 0 1 0 .336 5.99zm19.043 4.66c2.401 1.704 4.388 3.61 7.569 7.083a3 3 0 0 0 4.424-4.054c-3.448-3.763-5.686-5.911-8.522-7.923a3 3 0 1 0-3.471 4.894zm15.575 15.173c3.181 2.675 6.52 4.665 10.397 6.039a3 3 0 0 0 2.004-5.655c-3.162-1.121-5.884-2.743-8.54-4.976a3 3 0 1 0-3.861 4.592zm22.133 8.148c1.02.037 2.067.045 3.143.023a72.664 72.664 0 0 0 8.346-.638 3 3 0 1 0-.812-5.945c-2.442.334-4.996.53-7.658.585a48.55 48.55 0 0 1-2.796-.021 3 3 0 0 0-.223 5.996zm22.778-3.286c3.9-1.37 7.427-3.15 10.54-5.305a3 3 0 0 0-3.415-4.933c-2.665 1.845-5.712 3.382-9.114 4.578a3 3 0 0 0 1.989 5.66zm19.156-13.62a33.752 33.752 0 0 0 5.276-10.817 3 3 0 1 0-5.773-1.633 27.753 27.753 0 0 1-4.341 8.9 3 3 0 1 0 4.838 3.55zm6.577-22.657c-.187-3.817-.926-7.71-2.204-11.596a3 3 0 0 0-5.7 1.874c1.113 3.384 1.75 6.745 1.91 10.016a3 3 0 1 0 5.994-.294zm-7.097-22.26c-1.897-3.2-4.152-6.325-6.748-9.344a3 3 0 0 0-4.55 3.913c2.372 2.756 4.421 5.597 6.136 8.49a3 3 0 0 0 5.162-3.06zm-11.546-17.793c-.938-3.025-1.402-6.42-1.365-9.976a3 3 0 0 0-6-.063c-.043 4.163.506 8.177 1.634 11.816a3 3 0 1 0 5.731-1.777zm.053-20.107c.905-3.341 2.22-6.538 3.904-9.448a3 3 0 0 0-5.194-3.004c-1.948 3.368-3.463 7.048-4.501 10.884a3 3 0 1 0 5.791 1.568zm10.134-17.305c2.475-2.28 5.265-4.09 8.335-5.374a3 3 0 1 0-2.314-5.536c-3.725 1.558-7.105 3.75-10.086 6.497a3 3 0 1 0 4.065 4.413zm18.177-7.586c3.202-.18 6.599.092 10.18.843a3 3 0 0 0 1.23-5.872c-4.086-.857-8.009-1.172-11.747-.962a3 3 0 1 0 .337 5.99zm20.047 3.95c3.068 1.268 6.232 2.842 9.487 4.728a3 3 0 0 0 3.009-5.191c-3.48-2.017-6.883-3.71-10.204-5.083a3 3 0 1 0-2.292 5.545zm19.578 9.955c3.711 1.586 7.376 2.77 10.997 3.565a3 3 0 0 0 1.286-5.86c-3.248-.713-6.555-1.782-9.925-3.222a3 3 0 1 0-2.358 5.517zm22.591 4.789c3.94-.04 7.808-.553 11.61-1.513a3 3 0 1 0-1.468-5.817 43.358 43.358 0 0 1-10.203 1.33 3 3 0 0 0 .061 6zm22.52-5.558c3.335-1.637 6.607-3.613 9.845-5.916a3 3 0 1 0-3.477-4.89c-2.984 2.122-5.98 3.931-9.011 5.42a3 3 0 1 0 2.643 5.386zm18.678-13.054a3 3 0 0 1-4.02-4.454 130.547 130.547 0 0 0 5.31-5.088 3 3 0 1 1 4.265 4.22 136.507 136.507 0 0 1-5.555 5.322zm-48.722 25.641a3 3 0 1 1 4.314-4.17c3.056 3.16 5.075 6.744 6.172 10.754a3 3 0 0 1-5.787 1.584c-.834-3.047-2.35-5.739-4.699-8.168zm5.347 18.049a3 3 0 1 1 5.978.52c-.282 3.232-.805 6.273-1.832 11.206a3 3 0 0 1-5.874-1.222c.981-4.717 1.473-7.572 1.728-10.504zm-3.777 21.555a3 3 0 0 1 5.953.747c-.5 3.988-.397 7.09.399 9.67a3 3 0 1 1-5.733 1.769c-1.087-3.52-1.217-7.426-.62-12.186zm7.393 22.444a3 3 0 0 1 4.461-4.013c2.703 3.005 5.224 5.296 7.594 6.947a3 3 0 0 1-3.429 4.924c-2.775-1.932-5.632-4.53-8.626-7.858zm20.352 12.28a3 3 0 1 1 .334-5.99c2.77.154 5.453-.554 9.224-2.254a3 3 0 0 1 2.466 5.47c-4.57 2.06-8.103 2.993-12.024 2.775zm21.784-7.058a3 3 0 0 1-1.815-5.719c4.227-1.342 8.24-1.61 12.496-.572a3 3 0 0 1-1.421 5.83c-3.116-.76-6.025-.566-9.26.46zM106.53 56.038a3 3 0 1 1-3.45 4.909c-1.074-.755-6.723-6.044-8.083-7.204a68.019 68.019 0 0 0-.332-.281 3 3 0 1 1 3.865-4.59l.362.306c1.643 1.402 6.971 6.391 7.638 6.86zM88.536 42.422a3 3 0 0 1-2.285 5.548c-3.14-1.293-5.78-1.34-8.105-.05a3 3 0 0 1-2.91-5.247c4.087-2.266 8.597-2.187 13.3-.25zM66.698 48.73a3 3 0 0 1 2.029 5.647c-4.432 1.592-8.786.835-13.166-1.88a3 3 0 1 1 3.16-5.1c2.93 1.816 5.425 2.25 7.977 1.333zm-15.636-8.038a3 3 0 0 1-4.352 4.13c-.911-.96-1.85-1.98-3.061-3.32-.295-.325-2.437-2.703-3.07-3.4-.47-.518-.9-.988-1.313-1.436a3 3 0 0 1 4.41-4.068c.425.46.866.942 1.346 1.47.642.709 2.79 3.092 3.076 3.41a180.865 180.865 0 0 0 2.964 3.214z"/><path fill="#E1DBF1" d="M254.66 72.196l2-3.464a2 2 0 1 0-3.464-2l-2 3.464-3.464-2a2 2 0 0 0-2 3.464l3.464 2-2 3.464a2 2 0 0 0 3.464 2l2-3.464 3.464 2a2 2 0 1 0 2-3.464l-3.464-2zm-151.904 78.732l2.829-2.828a2 2 0 0 0-2.829-2.829l-2.828 2.829-2.828-2.829a2 2 0 0 0-2.829 2.829l2.829 2.828-2.829 2.829a2 2 0 1 0 2.829 2.828l2.828-2.828 2.828 2.828a2 2 0 1 0 2.829-2.828l-2.829-2.829z"/><path fill="#6B4FBB" d="M210.66 173.66l3.464-2a2 2 0 1 0-2-3.464l-3.464 2-2-3.464a2 2 0 0 0-3.464 2l2 3.464-3.464 2a2 2 0 1 0 2 3.464l3.464-2 2 3.464a2 2 0 1 0 3.464-2l-2-3.464z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M27 181a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm0-4a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M138 85a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M200 57a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#FC6D26" fill-rule="nonzero" d="M222.647 121.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M103.647 28.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FC6D26" fill-rule="nonzero" d="M85 103.488L81.841 108h6.318L85 103.488zm6.436 2.218A4 4 0 0 1 88.159 112H81.84a4 4 0 0 1-3.277-6.294l3.16-4.512a4 4 0 0 1 6.553 0l3.159 4.512z"/></g><path fill="#F9F9F9" d="M334.376 99.43A48.805 48.805 0 0 0 366 111c27.062 0 49-21.938 49-49s-21.938-49-49-49-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71z"/><path fill="#FFF" d="M339.376 94.43A48.805 48.805 0 0 0 371 106c27.062 0 49-21.938 49-49S398.062 8 371 8s-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71z"/><path fill="#EEE" fill-rule="nonzero" d="M329.85 99.072a4.5 4.5 0 0 1-5.516-5.517l2.827-10.48C322.501 75.258 320 66.31 320 57c0-28.167 22.833-51 51-51s51 22.833 51 51-22.833 51-51 51c-11.859 0-23.096-4.064-32.102-11.37l-9.048 2.442zm10.817-6.169C349.091 100.027 359.737 104 371 104c25.957 0 47-21.043 47-47s-21.043-47-47-47-47 21.043-47 47c0 8.859 2.453 17.351 7.016 24.716l.456.737-3.277 12.144c.072.527.347.685.613.613l11.059-2.984.8.677z"/><g transform="translate(354 34)"><path fill="#E1DBF1" fill-rule="nonzero" d="M13 4a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-8zm0-4h8a5 5 0 0 1 5 5v1a5 5 0 0 1-5 5h-8a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M5 11a1 1 0 0 0 0 2h24a1 1 0 0 0 0-2H5zm0-4h24a5 5 0 0 1 0 10H5A5 5 0 0 1 5 7z"/><rect width="12" height="4" x="11" y="31" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="19" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="37" fill="#E1DBF1" rx="2"/><rect width="12" height="4" x="11" y="43" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="25" fill="#E1DBF1" rx="2"/></g><path fill="#F9F9F9" d="M344.238 225.072A38.83 38.83 0 0 1 368 217c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213z"/><path fill="#FFF" d="M348.238 221.072A38.83 38.83 0 0 1 372 213c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213z"/><path fill="#EEE" fill-rule="nonzero" d="M336.85 215.928a4.5 4.5 0 0 0-5.516 5.517l3.543 13.13A40.848 40.848 0 0 0 331 252c0 22.644 18.356 41 41 41s41-18.356 41-41-18.356-41-41-41a40.82 40.82 0 0 0-24.182 7.887l-10.968-2.96zm12.608 6.73A36.824 36.824 0 0 1 372 215c20.435 0 37 16.565 37 37s-16.565 37-37 37-37-16.565-37-37c0-5.747 1.31-11.304 3.795-16.343l.334-.677-3.934-14.577a.5.5 0 0 1 .613-.613l12.865 3.471.785-.604z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M356.097 255.962a7 7 0 0 0 8.81 10.88l1.093-.885v1.454a7 7 0 1 0 14 0v-1.454l1.092.885a7 7 0 1 0 8.81-10.88l-1.185-.96 1.455-.337a7 7 0 1 0-3.15-13.64l-1.4.323.623-1.278a7 7 0 0 0-12.583-6.137l-.662 1.356-.662-1.356a7 7 0 0 0-12.583 6.137l.623 1.278-1.4-.324a7 7 0 1 0-3.15 13.641l1.455.336-1.186.96zm5.464-.913a11.914 11.914 0 0 1-.444-1.95l-.19-1.362-4.2-.97a3 3 0 0 1 1.35-5.845l4.178.964.768-1.145c.373-.557.793-1.082 1.254-1.57l.95-1.006-1.877-3.849a3 3 0 0 1 5.393-2.63l1.892 3.879 1.363-.113a12.188 12.188 0 0 1 2.004 0l1.363.113 1.892-3.879a3 3 0 0 1 5.393 2.63l-1.877 3.849.95 1.006c.461.488.88 1.013 1.254 1.57l.768 1.145 4.178-.964a3 3 0 1 1 1.35 5.846l-4.2.97-.19 1.36a11.914 11.914 0 0 1-.444 1.95l-.413 1.302 3.36 2.72a3 3 0 1 1-3.776 4.663l-3.32-2.688-1.196.706a11.94 11.94 0 0 1-1.808.873l-1.286.492v4.295a3 3 0 1 1-6 0v-4.295l-1.286-.492a11.94 11.94 0 0 1-1.808-.873l-1.196-.706-3.32 2.688a3 3 0 1 1-3.776-4.663l3.36-2.72-.413-1.301z"/><path fill="#FC6D26" fill-rule="nonzero" d="M373 245.411a6 6 0 1 0 0 12 6 6 0 0 0 0-12zm0 4a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/><g><path fill="#F9F9F9" d="M94.624 162.43A48.805 48.805 0 0 1 63 174c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71z"/><path fill="#FFF" stroke="#EEE" stroke-width="4" d="M89.624 157.43A48.805 48.805 0 0 1 58 169c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71z"/><path fill="#EEE" fill-rule="nonzero" d="M99.15 162.072a4.5 4.5 0 0 0 5.516-5.517l-2.827-10.48C106.499 138.258 109 129.31 109 120c0-28.167-22.833-51-51-51S7 91.833 7 120s22.833 51 51 51c11.859 0 23.096-4.064 32.102-11.37l9.048 2.442zm-10.817-6.169C79.909 163.027 69.263 167 58 167c-25.957 0-47-21.043-47-47s21.043-47 47-47 47 21.043 47 47c0 8.859-2.453 17.351-7.016 24.716l-.456.737 3.277 12.144c-.072.527-.347.685-.613.613l-11.059-2.984-.8.677z"/><g fill-rule="nonzero"><path fill="#FEE1D3" d="M55.47 94.47l-16.148 6.688a4 4 0 0 0-2.164 2.164l-6.689 16.147a4 4 0 0 0 0 3.062l6.689 16.147a4 4 0 0 0 2.164 2.164l16.147 6.689a4 4 0 0 0 3.062 0l16.147-6.689a4 4 0 0 0 2.164-2.164l6.689-16.147a4 4 0 0 0 0-3.062l-6.689-16.147a4 4 0 0 0-2.164-2.164L58.53 94.469a4 4 0 0 0-3.062 0zM57 98.164l16.147 6.688L79.835 121l-6.688 16.147L57 143.835l-16.147-6.688L34.165 121l6.688-16.147L57 98.165zM57 107a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4 11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12-6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 20c6.075 0 11-4.925 11-11s-4.925-11-11-11-11 4.925-11 11 4.925 11 11 11zm0-4a7 7 0 1 1 0-14 7 7 0 0 1 0 14z"/><path fill="#FC6D26" d="M57 126.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0-3a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></g></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/gitlab_logo.svg b/app/assets/images/illustrations/gitlab_logo.svg new file mode 100644 index 00000000000..8dbd75a340e --- /dev/null +++ b/app/assets/images/illustrations/gitlab_logo.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="492.509" height="453.68" viewBox="0 0 492.50943 453.67966"><g fill="none" fill-rule="evenodd"><path d="M491.589 259.398l-27.559-84.814L409.413 6.486c-2.81-8.648-15.045-8.648-17.856 0l-54.619 168.098H155.572L100.952 6.486c-2.81-8.648-15.046-8.648-17.856 0L28.478 174.584.921 259.398a18.775 18.775 0 0 0 6.82 20.992l238.513 173.29L484.77 280.39a18.777 18.777 0 0 0 6.82-20.992" fill="#fc6d26"/><path d="M246.255 453.68l90.684-279.096H155.57z" fill="#e24329"/><path d="M246.255 453.68L155.57 174.583H28.479z" fill="#fc6d26"/><path d="M28.479 174.584L.92 259.4a18.773 18.773 0 0 0 6.821 20.99l238.514 173.29z" fill="#fca326"/><path d="M28.479 174.584H155.57L100.952 6.487c-2.81-8.65-15.047-8.65-17.856 0z" fill="#e24329"/><path d="M246.255 453.68l90.684-279.096H464.03z" fill="#fc6d26"/><path d="M464.03 174.584l27.56 84.815a18.773 18.773 0 0 1-6.822 20.99L246.255 453.68z" fill="#fca326"/><path d="M464.03 174.584H336.94L391.557 6.487c2.811-8.65 15.047-8.65 17.856 0z" fill="#e24329"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/pipelines_pending.svg b/app/assets/images/illustrations/pipelines_pending.svg new file mode 100644 index 00000000000..25038366e92 --- /dev/null +++ b/app/assets/images/illustrations/pipelines_pending.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="430" height="220" viewBox="0 0 430 220"><g fill="none" fill-rule="evenodd"><path fill="#EEE" fill-rule="nonzero" d="M189.8 182l2.4-12H114c-5.523 0-10-4.477-10-10V34c0-5.523 4.477-10 10-10h200c5.523 0 10 4.477 10 10v126c0 5.523-4.477 10-10 10h-78.2l2.4 12h22.52a9.651 9.651 0 0 1 9.28 7 5.491 5.491 0 0 1-5.28 7H164.159a5.787 5.787 0 0 1-5.659-7 8.855 8.855 0 0 1 8.659-7H189.8zM114 28a6 6 0 0 0-6 6v126a6 6 0 0 0 6 6h200a6 6 0 0 0 6-6V34a6 6 0 0 0-6-6H114zm5 6h190a5 5 0 0 1 5 5v116a5 5 0 0 1-5 5H119a5 5 0 0 1-5-5V39a5 5 0 0 1 5-5zm0 4a1 1 0 0 0-1 1v116a1 1 0 0 0 1 1h190a1 1 0 0 0 1-1V39a1 1 0 0 0-1-1H119zm112.72 132h-35.44l-2.4 12h40.24l-2.4-12zm-64.561 16c-2.29 0-4.268 1.6-4.748 3.838A1.787 1.787 0 0 0 164.16 192h100.56a1.491 1.491 0 0 0 1.435-1.901A5.651 5.651 0 0 0 260.72 186h-93.561z"/><path fill="#FEF0E8" d="M177.965 99H194a2 2 0 1 1 0 4h-16.322c-1.374 6.29-6.976 11-13.678 11-6.702 0-12.304-4.71-13.678-11h-3.365l-7.395 9.249a2 2 0 0 1-3.049.089L128.11 103h-5.844a2 2 0 1 1 0-4H129a2 2 0 0 1 1.487.662l7.423 8.248 6.523-8.159a2 2 0 0 1 1.562-.751h4.04c.513-7.265 6.57-13 13.965-13 7.396 0 13.452 5.735 13.965 13zM164 110c5.523 0 10-4.477 10-10s-4.477-10-10-10-10 4.477-10 10 4.477 10 10 10z"/><path fill="#EFEDF8" d="M273.847 103c-.962 6.23-6.347 11-12.847 11-6.5 0-11.885-4.77-12.847-11H232a2 2 0 0 1 0-4h16.153c.962-6.23 6.347-11 12.847-11 6.5 0 11.885 4.77 12.847 11h3.998l8.404-9.338a2 2 0 0 1 3.048.09L296.692 99H305a2 2 0 0 1 0 4h-9.27a2 2 0 0 1-1.562-.751l-6.523-8.16-7.423 8.249a2 2 0 0 1-1.487.662h-4.888zM261 110a9 9 0 1 0 0-18 9 9 0 0 0 0 18z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M213 119c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19zm0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15z"/><path fill="#FC6D26" d="M211.586 101.828L208.757 99a2 2 0 1 0-2.828 2.828l4.243 4.243c.39.39.902.586 1.414.586.512 0 1.023-.195 1.414-.586L220.071 99a2 2 0 1 0-2.828-2.828l-5.657 5.656z"/><path fill="#FDC4A8" d="M162.95 101.07l-1.768-1.767a1.5 1.5 0 0 0-2.121 2.121l2.828 2.829c.293.293.677.439 1.06.439.385 0 .769-.146 1.062-.44l4.242-4.242a1.5 1.5 0 1 0-2.121-2.121l-3.182 3.182z"/><path fill="#6B4FBB" d="M256.39 104.841A6 6 0 1 0 261 95v6l-4.61 3.841z"/><path fill="#FEF0E8" fill-rule="nonzero" d="M99 99h-5a2 2 0 1 0 0 4h5a2 2 0 1 0 0-4zm-16 0h-5a2 2 0 1 0 0 4h5a2 2 0 1 0 0-4zm-14.384-.078l-3.643-3.425a2 2 0 1 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zm-11.657-10.96l-3.642-3.425a2 2 0 1 0-2.74 2.914l3.642 3.425a2 2 0 0 0 2.74-2.914zm-11.656-10.96l-3.643-3.425a2 2 0 0 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zm-14.367-3.885l-3.593 3.477a2 2 0 0 0 2.782 2.875l3.593-3.477a2 2 0 0 0-2.782-2.875zM19.44 84.244l-3.593 3.477a2 2 0 1 0 2.781 2.874l3.593-3.477a2 2 0 0 0-2.781-2.874zM7.94 95.371l-3.593 3.477a2 2 0 1 0 2.782 2.874l3.593-3.477a2 2 0 1 0-2.782-2.874z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M423.611 99.56l-3.598 3.472a2 2 0 0 0 2.777 2.879l3.599-3.472a2 2 0 0 0-2.778-2.878zm-11.514 11.11l-3.598 3.472a2 2 0 0 0 2.777 2.878l3.598-3.471a2 2 0 0 0-2.777-2.879zm-11.514 11.11l-3.599 3.471a2 2 0 1 0 2.778 2.879l3.598-3.472a2 2 0 1 0-2.777-2.879zm-8.799 4.48l-3.642-3.426a2 2 0 0 0-2.74 2.915l3.642 3.425a2 2 0 0 0 2.74-2.915zm-11.656-10.96l-3.643-3.426a2 2 0 1 0-2.74 2.914l3.643 3.426a2 2 0 1 0 2.74-2.915zm-11.657-10.96l-3.643-3.426a2 2 0 1 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zM353.001 99h-5a2 2 0 1 0 0 4h5a2 2 0 0 0 0-4zm-16 0h-5a2 2 0 1 0 0 4h5a2 2 0 0 0 0-4z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/slack_logo.svg b/app/assets/images/illustrations/slack_logo.svg new file mode 100644 index 00000000000..b8d7906c2e1 --- /dev/null +++ b/app/assets/images/illustrations/slack_logo.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="0 0 121.94154 121.84154" width="121.942" height="121.842"><style id="style200">.st0{fill:#ecb32d}.st1{fill:#63c1a0}.st2{fill:#e01a59}.st3{fill:#331433}.st4{fill:#d62027}.st5{fill:#89d3df}.st6{fill:#258b74}.st7{fill:#819c3c}</style><path class="st0" d="M79.03 7.511c-1.9-5.7-8-8.8-13.7-7-5.7 1.9-8.8 8-7 13.7l28.1 86.4c1.9 5.3 7.7 8.3 13.2 6.7 5.8-1.7 9.3-7.8 7.4-13.4 0-.2-28-86.4-28-86.4z" id="path202" fill="#ecb32d"/><path class="st1" d="M35.53 21.611c-1.9-5.7-8-8.8-13.7-7-5.7 1.9-8.8 8-7 13.7l28.1 86.4c1.9 5.3 7.7 8.3 13.2 6.7 5.8-1.7 9.3-7.8 7.4-13.4 0-.2-28-86.4-28-86.4z" id="path204" fill="#63c1a0"/><path class="st2" d="M114.43 79.011c5.7-1.9 8.8-8 7-13.7-1.9-5.7-8-8.8-13.7-7l-86.5 28.2c-5.3 1.9-8.3 7.7-6.7 13.2 1.7 5.8 7.8 9.3 13.4 7.4.2 0 86.5-28.1 86.5-28.1z" id="path206" fill="#e01a59"/><path class="st3" d="M39.23 103.511c5.6-1.8 12.9-4.2 20.7-6.7-1.8-5.6-4.2-12.9-6.7-20.7l-20.7 6.7z" id="path208" fill="#331433"/><path class="st4" d="M82.83 89.311c7.8-2.5 15.1-4.9 20.7-6.7-1.8-5.6-4.2-12.9-6.7-20.7l-20.7 6.7z" id="path210" fill="#d62027"/><path class="st5" d="M100.23 35.511c5.7-1.9 8.8-8 7-13.7-1.9-5.7-8-8.8-13.7-7l-86.4 28.1c-5.3 1.9-8.3 7.7-6.7 13.2 1.7 5.8 7.8 9.3 13.4 7.4.2 0 86.4-28 86.4-28z" id="path212" fill="#89d3df"/><path class="st6" d="M25.13 59.911c5.6-1.8 12.9-4.2 20.7-6.7-2.5-7.8-4.9-15.1-6.7-20.7l-20.7 6.7z" id="path214" fill="#258b74"/><path class="st7" d="M68.63 45.811c7.8-2.5 15.1-4.9 20.7-6.7-2.5-7.8-4.9-15.1-6.7-20.7l-20.7 6.7z" id="path216" fill="#819c3c"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/wiki-fro-logged-out-users.svg b/app/assets/images/illustrations/wiki-fro-logged-out-users.svg new file mode 100644 index 00000000000..c71841f72e5 --- /dev/null +++ b/app/assets/images/illustrations/wiki-fro-logged-out-users.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="412" height="260" viewBox="0 0 412 260" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M6.447.894L12 12H0L5.553.894a.5.5 0 0 1 .894 0z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#FEF0E8" fill-rule="nonzero" d="M338 50.287C322.695 41.45 303.124 46.694 294.287 62c-8.836 15.305-3.592 34.876 11.713 43.712 15.306 8.837 34.877 3.593 43.713-11.712 8.837-15.306 3.593-34.877-11.713-43.713zm2-3.464C357.22 56.763 363.118 78.78 353.177 96c-9.941 17.218-31.958 23.118-49.177 13.176-17.218-9.94-23.118-31.958-13.177-49.176C300.764 42.78 322.782 36.88 340 46.823z"/><g transform="rotate(-150 171.003 8.53)"><path fill="#FC6D26" fill-rule="nonzero" d="M4 16v25a2 2 0 1 0 4 0V16H4zm8-4v29a6 6 0 1 1-12 0V12h12z"/><use fill="#D8D8D8" xlink:href="#a"/><path stroke="#FDC4A8" stroke-width="4" d="M6 4.472L3.236 10h5.528L6 4.472z"/><path fill="#FC6D26" d="M9 6L6.447.894a.5.5 0 0 0-.894 0L3 6c.836.628 1.874 1 3 1a4.978 4.978 0 0 0 3-1z"/></g><path fill="#F9F9F9" d="M263.116 237.116A10.002 10.002 0 0 1 254 243h-86c-11.046 0-20-8.954-20-20V121c0-4.056 2.414-7.547 5.884-9.116A9.964 9.964 0 0 0 153 116v106c0 8.837 7.163 16 16 16h90c1.467 0 2.86-.316 4.116-.884z"/><path fill="#EEE" fill-rule="nonzero" d="M214.5 106H163c-5.523 0-10 4.477-10 10v106c0 8.837 7.163 16 16 16h90c5.523 0 10-4.477 10-10v-17.999a10.036 10.036 0 0 1-4 3.167V228a6 6 0 0 1-6 6h-90c-6.627 0-12-5.373-12-12V116a6 6 0 0 1 6-6h7v-4h44.5z"/><path fill="#EEE" fill-rule="nonzero" d="M260 218.268V214h-90a6 6 0 0 0 0 12h86a4 4 0 0 0 4-4v-.268a1.99 1.99 0 0 1-1 .268h-50a2 2 0 0 1 0-4h50c.364 0 .706.097 1 .268zM170 210h90.5a3.5 3.5 0 0 1 3.5 3.5v8.5a8 8 0 0 1-8 8h-86c-5.523 0-10-4.477-10-10s4.477-10 10-10z"/><path fill="#EEE" fill-rule="nonzero" d="M174 110v100h87a6 6 0 0 0 6-6v-88a6 6 0 0 0-6-6h-87zm-4-4h91c5.523 0 10 4.477 10 10v88c0 5.523-4.477 10-10 10h-91V106z"/><path fill="#EFEDF8" d="M230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M236.182 129.207a5.5 5.5 0 0 1 6.102.04l7.716 5.219V105a2 2 0 0 0-2-2h-18a2 2 0 0 0-2 2v29.584l8.182-5.377zM230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><g fill-rule="nonzero"><path fill="#EFEDF8" d="M156 74c14.912 0 27-12.088 27-27s-12.088-27-27-27-27 12.088-27 27 12.088 27 27 27zm0 4c-17.12 0-31-13.88-31-31s13.88-31 31-31 31 13.88 31 31-13.88 31-31 31z"/><path fill="#6B4FBB" d="M147.535 44.916l-.116 1.086a8.446 8.446 0 0 0 .093 2.44l.2 1.08-2.262 1.202a.495.495 0 0 0-.213.678l.941 1.77c.128.239.434.332.68.201l2.25-1.196.785.775a8.544 8.544 0 0 0 1.967 1.45l.975.522-.486 2.5a.495.495 0 0 0 .392.59l1.968.383a.504.504 0 0 0 .585-.401l.489-2.515 1.086-.13a8.584 8.584 0 0 0 2.363-.633l1.005-.43 1.68 1.933a.495.495 0 0 0 .708.055l1.513-1.315a.504.504 0 0 0 .044-.708l-1.67-1.922.583-.94c.431-.696.761-1.45.978-2.239l.292-1.063 2.547-.089a.495.495 0 0 0 .488-.515l-.07-2.003a.504.504 0 0 0-.523-.48l-2.56.09-.367-1.037a8.446 8.446 0 0 0-1.139-2.159l-.644-.882 1.509-2.076a.495.495 0 0 0-.106-.702l-1.621-1.178a.504.504 0 0 0-.7.116l-1.494 2.057-1.05-.362a8.459 8.459 0 0 0-2.398-.455l-1.1-.047-.66-2.466a.495.495 0 0 0-.613-.36l-1.936.519a.504.504 0 0 0-.35.617l.661 2.466-.93.59a8.459 8.459 0 0 0-1.848 1.594l-.728.838-2.322-1.034a.495.495 0 0 0-.665.25l-.815 1.83a.504.504 0 0 0 .26.661l2.344 1.044zm-3.565 1.697a3.504 3.504 0 0 1-1.78-4.622l.815-1.83a3.495 3.495 0 0 1 4.626-1.77l.346.154c.259-.245.529-.477.81-.697l-.106-.394a3.504 3.504 0 0 1 2.471-4.292l1.936-.519a3.495 3.495 0 0 1 4.286 2.481l.106.395c.353.05.703.116 1.05.198l.222-.306a3.504 3.504 0 0 1 4.89-.78l1.622 1.178a3.495 3.495 0 0 1 .769 4.892l-.258.355c.184.312.354.633.508.962l.42-.014a3.504 3.504 0 0 1 3.625 3.373l.07 2.003a3.495 3.495 0 0 1-3.382 3.618l-.4.014c-.127.332-.27.659-.426.978l.256.294a3.504 3.504 0 0 1-.34 4.941l-1.512 1.315a3.495 3.495 0 0 1-4.94-.351l-.283-.325a11.669 11.669 0 0 1-1.05.28l-.082.424a3.504 3.504 0 0 1-4.103 2.774l-1.967-.382a3.495 3.495 0 0 1-2.765-4.11l.075-.383a11.547 11.547 0 0 1-.858-.633l-.354.188a3.504 3.504 0 0 1-4.738-1.442l-.94-1.77a3.495 3.495 0 0 1 1.453-4.734l.37-.197a11.436 11.436 0 0 1-.041-1.088l-.4-.178zm13.326 5.608a5.5 5.5 0 1 1-2.847-10.625 5.5 5.5 0 0 1 2.847 10.625zm-.776-2.898a2.5 2.5 0 1 0-1.294-4.83 2.5 2.5 0 0 0 1.294 4.83z"/></g><g fill-rule="nonzero"><path fill="#EFEDF8" d="M326.979 222.047c14.403 3.86 29.209-4.688 33.068-19.092 3.86-14.403-4.688-29.209-19.092-33.068-14.403-3.86-29.209 4.688-33.068 19.092-3.86 14.404 4.688 29.209 19.092 33.068zm-1.035 3.864c-16.538-4.431-26.352-21.43-21.92-37.967 4.43-16.538 21.429-26.352 37.966-21.92 16.538 4.43 26.352 21.429 21.92 37.966-4.43 16.538-21.429 26.352-37.966 21.92z"/><path fill="#6B4FBB" d="M329.376 201.598c-4.668-2.621-7.155-8.157-5.706-13.566 1.715-6.402 8.295-10.201 14.697-8.486 6.402 1.716 10.2 8.296 8.485 14.697-1.45 5.41-6.371 8.96-11.725 8.897a3.03 3.03 0 0 1-.074.365l-1.812 6.761a3 3 0 0 1-5.795-1.552l1.812-6.762a3.03 3.03 0 0 1 .118-.354zm3.815-2.733a8 8 0 1 0 4.14-15.455 8 8 0 0 0-4.14 15.455z"/></g><path fill="#FEF0E8" fill-rule="nonzero" d="M91.373 193c17.071-4.574 27.202-22.12 22.628-39.191-4.575-17.071-22.121-27.202-39.192-22.628-17.071 4.574-27.202 22.121-22.628 39.192 4.574 17.071 22.121 27.202 39.192 22.627zm1.035 3.864c-19.204 5.146-38.945-6.25-44.09-25.456-5.146-19.204 6.25-38.945 25.455-44.09 19.205-5.146 38.945 6.25 44.091 25.455 5.146 19.205-6.25 38.945-25.456 44.091z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M70.067 152.122l6.73 25.114 19.318-5.176-6.73-25.114-19.318 5.176zm-1.035-3.864l19.318-5.176a4 4 0 0 1 4.9 2.828l6.729 25.114a4 4 0 0 1-2.829 4.9L77.832 181.1a4 4 0 0 1-4.9-2.829l-6.729-25.114a4 4 0 0 1 2.829-4.899z"/><path fill="#FC6D26" d="M76.898 154.433l7.727-2.07a2 2 0 0 1 1.036 3.863l-7.728 2.07a2 2 0 1 1-1.035-3.863zm1.812 6.761l5.795-1.553a2 2 0 0 1 1.035 3.864l-5.795 1.553a2 2 0 1 1-1.035-3.864zm1.811 6.762l7.728-2.07a2 2 0 0 1 1.035 3.863l-7.727 2.07a2 2 0 1 1-1.036-3.863z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 38d1effc77c..d963101028a 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -15,6 +15,8 @@ const Api = {    issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',    usersPath: '/api/:version/users.json',    commitPath: '/api/:version/projects/:id/repository/commits', +  branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', +  createBranchPath: '/api/:version/projects/:id/repository/branches',    group(groupId, callback) {      const url = Api.buildUrl(Api.groupPath) @@ -123,6 +125,19 @@ const Api = {      });    }, +  branchSingle(id, branch) { +    const url = Api.buildUrl(Api.branchSinglePath) +      .replace(':id', id) +      .replace(':branch', branch); + +    return this.wrapAjaxCall({ +      url, +      type: 'GET', +      contentType: 'application/json; charset=utf-8', +      dataType: 'json', +    }); +  }, +    // Return text for a specific license    licenseText(key, data, callback) {      const url = Api.buildUrl(Api.licensePath) diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index 4d2d4db7c0e..0f28bd233ac 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -1,8 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */ +/* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */ +  import AccessorUtilities from './lib/utils/accessor'; -window.Autosave = (function() { -  function Autosave(field, key, resource) { +export default class Autosave { +  constructor(field, key, resource) {      this.field = field;      this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();      this.resource = resource; @@ -12,14 +13,10 @@ window.Autosave = (function() {      this.key = 'autosave/' + key;      this.field.data('autosave', this);      this.restore(); -    this.field.on('input', (function(_this) { -      return function() { -        return _this.save(); -      }; -    })(this)); +    this.field.on('input', () => this.save());    } -  Autosave.prototype.restore = function() { +  restore() {      var text;      if (!this.isLocalStorageAvailable) return; @@ -40,9 +37,9 @@ window.Autosave = (function() {          field.dispatchEvent(event);        }      } -  }; +  } -  Autosave.prototype.save = function() { +  save() {      var text;      text = this.field.val(); @@ -51,15 +48,11 @@ window.Autosave = (function() {      }      return this.reset(); -  }; +  } -  Autosave.prototype.reset = function() { +  reset() {      if (!this.isLocalStorageAvailable) return;      return window.localStorage.removeItem(this.key); -  }; - -  return Autosave; -})(); - -export default window.Autosave; +  } +} diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 4f01345ee3b..622764107ad 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,8 +1,8 @@  /* eslint-disable class-methods-use-this */ -/* global Flash */  import _ from 'underscore';  import Cookies from 'js-cookie';  import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; +import Flash from './flash';  const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';  const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index e00af4b2fa8..add43b81f6d 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,8 +1,8 @@ -import autosize from 'vendor/autosize'; +import Autosize from 'autosize';  document.addEventListener('DOMContentLoaded', () => {    const autosizeEls = document.querySelectorAll('.js-autosize'); -  autosize(autosizeEls); -  autosize.update(autosizeEls); +  Autosize(autosizeEls); +  Autosize.update(autosizeEls);  }); diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/behaviors/copy_as_gfm.js index 93b0cbf4209..e7dc4ef8304 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/copy_as_gfm.js @@ -1,7 +1,8 @@  /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ +  import _ from 'underscore'; -import { insertText, getSelectedFragment, nodeMatchesSelector } from './lib/utils/common_utils'; -import { placeholderImage } from './lazy_loader'; +import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils'; +import { placeholderImage } from '../lazy_loader';  const gfmRules = {    // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert @@ -284,7 +285,7 @@ const gfmRules = {    },  }; -class CopyAsGFM { +export class CopyAsGFM {    constructor() {      $(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });      $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); @@ -469,7 +470,12 @@ class CopyAsGFM {    }  } -window.gl = window.gl || {}; -window.gl.CopyAsGFM = CopyAsGFM; +// Export CopyAsGFM as a global for rspec to access +// see /spec/features/copy_as_gfm_spec.rb +if (process.env.NODE_ENV !== 'production') { +  window.CopyAsGFM = CopyAsGFM; +} -new CopyAsGFM(); +export default function initCopyAsGFM() { +  return new CopyAsGFM(); +} diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 44b2c974b9e..671532394a9 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -1,5 +1,6 @@  import './autosize';  import './bind_in_out'; +import initCopyAsGFM from './copy_as_gfm';  import './details_behavior';  import installGlEmojiElement from './gl_emoji';  import './quick_submit'; @@ -7,3 +8,4 @@ import './requires_input';  import './toggler_behavior';  installGlEmojiElement(); +initCopyAsGFM(); diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js index 8641a6fdae6..062577af385 100644 --- a/app/assets/javascripts/blob/balsamiq_viewer.js +++ b/app/assets/javascripts/blob/balsamiq_viewer.js @@ -1,9 +1,8 @@ -/* global Flash */ - +import Flash from '../flash';  import BalsamiqViewer from './balsamiq/balsamiq_viewer';  function onError() { -  const flash = new window.Flash('Balsamiq file could not be loaded.'); +  const flash = new Flash('Balsamiq file could not be loaded.');    return flash;  } diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index ddd1fea3aca..0d590a9dbc4 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -1,6 +1,5 @@  /* eslint-disable func-names, object-shorthand, prefer-arrow-callback */ -/* global Dropzone */ - +import Dropzone from 'dropzone';  import '../lib/utils/url_utility';  import { HIDDEN_CLASS } from '../lib/utils/constants';  import csrf from '../lib/utils/csrf'; diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index a20c6ca7a21..583e5faa506 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -1,6 +1,5 @@  /* eslint-disable class-methods-use-this */ -/* global Flash */ - +import Flash from '../flash';  import FileTemplateTypeSelector from './template_selectors/type_selector';  import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';  import DockerfileSelector from './template_selectors/dockerfile_selector'; diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index e0b73f13d36..54132e8537b 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -1,4 +1,4 @@ -/* global Flash */ +import Flash from '../../flash';  import { handleLocationHash } from '../../lib/utils/common_utils';  export default class BlobViewer { diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 815248f38ee..ef4093b59e3 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -1,10 +1,10 @@  /* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */  /* global BoardService */ -/* global Flash */  import _ from 'underscore';  import Vue from 'vue';  import VueResource from 'vue-resource'; +import Flash from '../flash';  import FilteredSearchBoards from './filtered_search_boards';  import eventHub from './eventhub';  import './models/issue'; diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 590b7be36e3..9ae5e270a4b 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -1,15 +1,16 @@  /* eslint-disable comma-dangle, space-before-function-paren, no-new */ -/* global IssuableContext */  /* global MilestoneSelect */ -/* global LabelsSelect */  /* global Sidebar */ -/* global Flash */  import Vue from 'vue'; +import Flash from '../../flash';  import eventHub from '../../sidebar/event_hub';  import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';  import Assignees from '../../sidebar/components/assignees/assignees'; +import DueDateSelectors from '../../due_date_select';  import './sidebar/remove_issue'; +import IssuableContext from '../../issuable_context'; +import LabelsSelect from '../../labels_select';  const Store = gl.issueBoards.BoardsStore; @@ -113,7 +114,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({    mounted () {      new IssuableContext(this.currentUser);      new MilestoneSelect(); -    new gl.DueDateSelectors(); +    new DueDateSelectors();      new LabelsSelect();      new Sidebar();      gl.Subscription.bindAll('.subscription'); diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index a656f0546c0..de9e44cef35 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -1,7 +1,7 @@  /* eslint-disable no-new */ -/* global Flash */  import Vue from 'vue'; +import Flash from '../../../flash';  import './lists_dropdown';  const ModalStore = gl.issueBoards.ModalStore; diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index 1e623cf58b7..1ad97211934 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -1,7 +1,7 @@  /* eslint-disable no-new */ -/* global Flash */  import Vue from 'vue'; +import Flash from '../../../flash';  const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 3f083655f95..184665f395c 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {      // Issue boards is slightly different, we handle all the requests async      // instead or reloading the page, we just re-fire the list ajax requests      this.isHandledAsync = true; -    this.cantEdit = cantEdit; +    this.cantEdit = cantEdit.filter(i => typeof i === 'string'); +    this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object');    }    updateObject(path) { @@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {      this.filteredSearchInput.dispatchEvent(new Event('input'));    } -  canEdit(tokenName) { -    return this.cantEdit.indexOf(tokenName) === -1; +  canEdit(tokenName, tokenValue) { +    if (this.cantEdit.includes(tokenName)) return false; +    return this.cantEditWithValue.findIndex(token => token.name === tokenName && +      token.value === tokenValue) === -1;    }  } diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 38eea38f949..97e80afa3f8 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -7,7 +7,7 @@ class BoardService {      this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {        issues: {          method: 'GET', -        url: `${gon.relative_url_root}/boards/${boardId}/issues.json`, +        url: `${gon.relative_url_root}/-/boards/${boardId}/issues.json`,        }      });      this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, { @@ -16,7 +16,7 @@ class BoardService {          url: `${listsEndpoint}/generate.json`        }      }); -    this.issue = Vue.resource(`${gon.relative_url_root}/boards/${boardId}/issues{/id}`, {}); +    this.issue = Vue.resource(`${gon.relative_url_root}/-/boards/${boardId}/issues{/id}`, {});      this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {        bulkUpdate: {          method: 'POST', diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index ea82958e80d..798d7e0d147 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -14,16 +14,18 @@ gl.issueBoards.BoardsStore = {    },    state: {},    detail: { -    issue: {} +    issue: {},    },    moving: {      issue: {}, -    list: {} +    list: {},    },    create () {      this.state.lists = [];      this.filter.path = getUrlParamsArray().join('&'); -    this.detail = { issue: {} }; +    this.detail = { +      issue: {}, +    };    },    addList (listObj, defaultAvatar) {      const list = new List(listObj, defaultAvatar); diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js index f73e489e7b2..ff88083a4b4 100644 --- a/app/assets/javascripts/broadcast_message.js +++ b/app/assets/javascripts/broadcast_message.js @@ -1,33 +1,28 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */ - -$(function() { -  var previewPath; -  $('input#broadcast_message_color').on('input', function() { -    var previewColor; -    previewColor = $(this).val(); -    return $('div.broadcast-message-preview').css('background-color', previewColor); +export default function initBroadcastMessagesForm() { +  $('input#broadcast_message_color').on('input', function onMessageColorInput() { +    const previewColor = $(this).val(); +    $('div.broadcast-message-preview').css('background-color', previewColor);    }); -  $('input#broadcast_message_font').on('input', function() { -    var previewColor; -    previewColor = $(this).val(); -    return $('div.broadcast-message-preview').css('color', previewColor); + +  $('input#broadcast_message_font').on('input', function onMessageFontInput() { +    const previewColor = $(this).val(); +    $('div.broadcast-message-preview').css('color', previewColor);    }); -  previewPath = $('textarea#broadcast_message_message').data('preview-path'); -  return $('textarea#broadcast_message_message').on('input', function() { -    var message; -    message = $(this).val(); + +  const previewPath = $('textarea#broadcast_message_message').data('preview-path'); + +  $('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() { +    const message = $(this).val();      if (message === '') { -      return $('.js-broadcast-message-preview').text("Your message here"); +      $('.js-broadcast-message-preview').text('Your message here');      } else { -      return $.ajax({ +      $.ajax({          url: previewPath, -        type: "POST", +        type: 'POST',          data: { -          broadcast_message: { -            message: message -          } -        } +          broadcast_message: { message }, +        },        });      } -  }); -}); +  }, 250)); +} diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index 19388f1f9ae..ace89398943 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -1,30 +1,30 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */ +/* eslint-disable func-names, prefer-arrow-callback, no-return-assign */  import { visitUrl } from './lib/utils/url_utility';  import { convertPermissionToBoolean } from './lib/utils/common_utils'; -window.BuildArtifacts = (function() { -  function BuildArtifacts() { +export default class BuildArtifacts { +  constructor() {      this.disablePropagation();      this.setupEntryClick();      this.setupTooltips();    } - -  BuildArtifacts.prototype.disablePropagation = function() { -    $('.top-block').on('click', '.download', function(e) { +  // eslint-disable-next-line class-methods-use-this +  disablePropagation() { +    $('.top-block').on('click', '.download', function (e) {        return e.stopPropagation();      }); -    return $('.tree-holder').on('click', 'tr[data-link] a', function(e) { +    return $('.tree-holder').on('click', 'tr[data-link] a', function (e) {        return e.stopImmediatePropagation();      }); -  }; - -  BuildArtifacts.prototype.setupEntryClick = function() { -    return $('.tree-holder').on('click', 'tr[data-link]', function(e) { +  } +  // eslint-disable-next-line class-methods-use-this +  setupEntryClick() { +    return $('.tree-holder').on('click', 'tr[data-link]', function () {        visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink));      }); -  }; - -  BuildArtifacts.prototype.setupTooltips = function() { +  } +  // eslint-disable-next-line class-methods-use-this +  setupTooltips() {      $('.js-artifact-tree-tooltip').tooltip({        placement: 'bottom',        // Stop the tooltip from hiding when we stop hovering the element directly @@ -41,7 +41,5 @@ window.BuildArtifacts = (function() {        .on('mouseleave', (e) => {          $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide');        }); -  }; - -  return BuildArtifacts; -})(); +  } +} diff --git a/app/assets/javascripts/build_variables.js b/app/assets/javascripts/build_variables.js index c955a9ac2ea..35edf3e0017 100644 --- a/app/assets/javascripts/build_variables.js +++ b/app/assets/javascripts/build_variables.js @@ -1,8 +1,10 @@ -/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren */ +/* eslint-disable func-names*/ -$(function() { -  $('.reveal-variables').off('click').on('click', function() { -    $('.js-build-variables').toggle(); -    $(this).hide(); -  }); -}); +export default function handleRevealVariables() { +  $('.js-reveal-variables') +    .off('click') +    .on('click', function () { +      $('.js-build-variables').toggle(); +      $(this).hide(); +    }); +} diff --git a/app/assets/javascripts/ci_lint_editor.js b/app/assets/javascripts/ci_lint_editor.js index dd4a08a2f31..b9469e5b7cb 100644 --- a/app/assets/javascripts/ci_lint_editor.js +++ b/app/assets/javascripts/ci_lint_editor.js @@ -1,7 +1,4 @@ - -window.gl = window.gl || {}; - -class CILintEditor { +export default class CILintEditor {    constructor() {      this.editor = window.ace.edit('ci-editor');      this.textarea = document.querySelector('#content'); @@ -13,5 +10,3 @@ class CILintEditor {      });    }  } - -gl.CILintEditor = CILintEditor; diff --git a/app/assets/javascripts/clusters.js b/app/assets/javascripts/clusters.js deleted file mode 100644 index 50dbeb06362..00000000000 --- a/app/assets/javascripts/clusters.js +++ /dev/null @@ -1,112 +0,0 @@ -/* globals Flash */ -import Visibility from 'visibilityjs'; -import axios from 'axios'; -import Poll from './lib/utils/poll'; -import { s__ } from './locale'; -import './flash'; - -/** - * Cluster page has 2 separate parts: - *   Toggle button - * - * - Polling status while creating or scheduled - * -- Update status area with the response result - */ - -class ClusterService { -  constructor(options = {}) { -    this.options = options; -  } -  fetchData() { -    return axios.get(this.options.endpoint); -  } -} - -export default class Clusters { -  constructor() { -    const dataset = document.querySelector('.js-edit-cluster-form').dataset; - -    this.state = { -      statusPath: dataset.statusPath, -      clusterStatus: dataset.clusterStatus, -      clusterStatusReason: dataset.clusterStatusReason, -      toggleStatus: dataset.toggleStatus, -    }; - -    this.service = new ClusterService({ endpoint: this.state.statusPath }); -    this.toggleButton = document.querySelector('.js-toggle-cluster'); -    this.toggleInput = document.querySelector('.js-toggle-input'); -    this.errorContainer = document.querySelector('.js-cluster-error'); -    this.successContainer = document.querySelector('.js-cluster-success'); -    this.creatingContainer = document.querySelector('.js-cluster-creating'); -    this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason'); - -    this.toggleButton.addEventListener('click', this.toggle.bind(this)); - -    if (this.state.clusterStatus !== 'created') { -      this.updateContainer(this.state.clusterStatus, this.state.clusterStatusReason); -    } - -    if (this.state.statusPath) { -      this.initPolling(); -    } -  } - -  toggle() { -    this.toggleButton.classList.toggle('checked'); -    this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString()); -  } - -  initPolling() { -    this.poll = new Poll({ -      resource: this.service, -      method: 'fetchData', -      successCallback: (data) => { -        const { status, status_reason } = data.data; -        this.updateContainer(status, status_reason); -      }, -      errorCallback: () => { -        Flash(s__('ClusterIntegration|Something went wrong on our end.')); -      }, -    }); - -    if (!Visibility.hidden()) { -      this.poll.makeRequest(); -    } else { -      this.service.fetchData(); -    } - -    Visibility.change(() => { -      if (!Visibility.hidden()) { -        this.poll.restart(); -      } else { -        this.poll.stop(); -      } -    }); -  } - -  hideAll() { -    this.errorContainer.classList.add('hidden'); -    this.successContainer.classList.add('hidden'); -    this.creatingContainer.classList.add('hidden'); -  } - -  updateContainer(status, error) { -    this.hideAll(); -    switch (status) { -      case 'created': -        this.successContainer.classList.remove('hidden'); -        break; -      case 'errored': -        this.errorContainer.classList.remove('hidden'); -        this.errorReasonContainer.textContent = error; -        break; -      case 'scheduled': -      case 'creating': -        this.creatingContainer.classList.remove('hidden'); -        break; -      default: -        this.hideAll(); -    } -  } -} diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js new file mode 100644 index 00000000000..dc443475952 --- /dev/null +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -0,0 +1,221 @@ +import Visibility from 'visibilityjs'; +import Vue from 'vue'; +import { s__, sprintf } from '../locale'; +import Flash from '../flash'; +import Poll from '../lib/utils/poll'; +import initSettingsPanels from '../settings_panels'; +import eventHub from './event_hub'; +import { +  APPLICATION_INSTALLED, +  REQUEST_LOADING, +  REQUEST_SUCCESS, +  REQUEST_FAILURE, +} from './constants'; +import ClustersService from './services/clusters_service'; +import ClustersStore from './stores/clusters_store'; +import applications from './components/applications.vue'; + +/** + * Cluster page has 2 separate parts: + * Toggle button and applications section + * + * - Polling status while creating or scheduled + * - Update status area with the response result + */ + +export default class Clusters { +  constructor() { +    const { +      statusPath, +      installHelmPath, +      installIngressPath, +      installRunnerPath, +      clusterStatus, +      clusterStatusReason, +      helpPath, +    } = document.querySelector('.js-edit-cluster-form').dataset; + +    this.store = new ClustersStore(); +    this.store.setHelpPath(helpPath); +    this.store.updateStatus(clusterStatus); +    this.store.updateStatusReason(clusterStatusReason); +    this.service = new ClustersService({ +      endpoint: statusPath, +      installHelmEndpoint: installHelmPath, +      installIngressEndpoint: installIngressPath, +      installRunnerEndpoint: installRunnerPath, +    }); + +    this.toggle = this.toggle.bind(this); +    this.installApplication = this.installApplication.bind(this); + +    this.toggleButton = document.querySelector('.js-toggle-cluster'); +    this.toggleInput = document.querySelector('.js-toggle-input'); +    this.errorContainer = document.querySelector('.js-cluster-error'); +    this.successContainer = document.querySelector('.js-cluster-success'); +    this.creatingContainer = document.querySelector('.js-cluster-creating'); +    this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason'); +    this.successApplicationContainer = document.querySelector('.js-cluster-application-notice'); + +    initSettingsPanels(); +    this.initApplications(); + +    if (this.store.state.status !== 'created') { +      this.updateContainer(null, this.store.state.status, this.store.state.statusReason); +    } + +    this.addListeners(); +    if (statusPath) { +      this.initPolling(); +    } +  } + +  initApplications() { +    const store = this.store; +    const el = document.querySelector('#js-cluster-applications'); + +    this.applications = new Vue({ +      el, +      components: { +        applications, +      }, +      data() { +        return { +          state: store.state, +        }; +      }, +      render(createElement) { +        return createElement('applications', { +          props: { +            applications: this.state.applications, +            helpPath: this.state.helpPath, +          }, +        }); +      }, +    }); +  } + +  addListeners() { +    this.toggleButton.addEventListener('click', this.toggle); +    eventHub.$on('installApplication', this.installApplication); +  } + +  removeListeners() { +    this.toggleButton.removeEventListener('click', this.toggle); +    eventHub.$off('installApplication', this.installApplication); +  } + +  initPolling() { +    this.poll = new Poll({ +      resource: this.service, +      method: 'fetchData', +      successCallback: data => this.handleSuccess(data), +      errorCallback: () => Clusters.handleError(), +    }); + +    if (!Visibility.hidden()) { +      this.poll.makeRequest(); +    } else { +      this.service.fetchData() +        .then(data => this.handleSuccess(data)) +        .catch(() => Clusters.handleError()); +    } + +    Visibility.change(() => { +      if (!Visibility.hidden() && !this.destroyed) { +        this.poll.restart(); +      } else { +        this.poll.stop(); +      } +    }); +  } + +  static handleError() { +    Flash(s__('ClusterIntegration|Something went wrong on our end.')); +  } + +  handleSuccess(data) { +    const prevStatus = this.store.state.status; +    const prevApplicationMap = Object.assign({}, this.store.state.applications); + +    this.store.updateStateFromServer(data.data); + +    this.checkForNewInstalls(prevApplicationMap, this.store.state.applications); +    this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason); +  } + +  toggle() { +    this.toggleButton.classList.toggle('checked'); +    this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString()); +  } + +  hideAll() { +    this.errorContainer.classList.add('hidden'); +    this.successContainer.classList.add('hidden'); +    this.creatingContainer.classList.add('hidden'); +  } + +  checkForNewInstalls(prevApplicationMap, newApplicationMap) { +    const appTitles = Object.keys(newApplicationMap) +      .filter(appId => newApplicationMap[appId].status === APPLICATION_INSTALLED && +        prevApplicationMap[appId].status !== APPLICATION_INSTALLED && +        prevApplicationMap[appId].status !== null) +      .map(appId => newApplicationMap[appId].title); + +    if (appTitles.length > 0) { +      const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your cluster'), { +        appList: appTitles.join(', '), +      }); +      Flash(text, 'notice', this.successApplicationContainer); +    } +  } + +  updateContainer(prevStatus, status, error) { +    this.hideAll(); + +    // We poll all the time but only want the `created` banner to show when newly created +    if (this.store.state.status !== 'created' || prevStatus !== this.store.state.status) { +      switch (status) { +        case 'created': +          this.successContainer.classList.remove('hidden'); +          break; +        case 'errored': +          this.errorContainer.classList.remove('hidden'); +          this.errorReasonContainer.textContent = error; +          break; +        case 'scheduled': +        case 'creating': +          this.creatingContainer.classList.remove('hidden'); +          break; +        default: +          this.hideAll(); +      } +    } +  } + +  installApplication(appId) { +    this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING); +    this.store.updateAppProperty(appId, 'requestReason', null); + +    this.service.installApplication(appId) +      .then(() => { +        this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS); +      }) +      .catch(() => { +        this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE); +        this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed')); +      }); +  } + +  destroy() { +    this.destroyed = true; + +    this.removeListeners(); + +    if (this.poll) { +      this.poll.stop(); +    } + +    this.applications.$destroy(); +  } +} diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue new file mode 100644 index 00000000000..872abf03ef1 --- /dev/null +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -0,0 +1,185 @@ +<script> +import { s__, sprintf } from '../../locale'; +import eventHub from '../event_hub'; +import loadingButton from '../../vue_shared/components/loading_button.vue'; +import { +  APPLICATION_NOT_INSTALLABLE, +  APPLICATION_SCHEDULED, +  APPLICATION_INSTALLABLE, +  APPLICATION_INSTALLING, +  APPLICATION_INSTALLED, +  APPLICATION_ERROR, +  REQUEST_LOADING, +  REQUEST_SUCCESS, +  REQUEST_FAILURE, +} from '../constants'; + +export default { +  props: { +    id: { +      type: String, +      required: true, +    }, +    title: { +      type: String, +      required: true, +    }, +    titleLink: { +      type: String, +      required: false, +    }, +    description: { +      type: String, +      required: true, +    }, +    status: { +      type: String, +      required: false, +    }, +    statusReason: { +      type: String, +      required: false, +    }, +    requestStatus: { +      type: String, +      required: false, +    }, +    requestReason: { +      type: String, +      required: false, +    }, +  }, +  components: { +    loadingButton, +  }, +  computed: { +    rowJsClass() { +      return `js-cluster-application-row-${this.id}`; +    }, +    installButtonLoading() { +      return !this.status || +        this.status === APPLICATION_SCHEDULED || +        this.status === APPLICATION_INSTALLING || +        this.requestStatus === REQUEST_LOADING; +    }, +    installButtonDisabled() { +      // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but +      // we already made a request to install and are just waiting for the real-time +      // to sync up. +      return (this.status !== APPLICATION_INSTALLABLE && this.status !== APPLICATION_ERROR) || +        this.requestStatus === REQUEST_LOADING || +        this.requestStatus === REQUEST_SUCCESS; +    }, +    installButtonLabel() { +      let label; +      if ( +        this.status === APPLICATION_NOT_INSTALLABLE || +        this.status === APPLICATION_INSTALLABLE || +        this.status === APPLICATION_ERROR +      ) { +        label = s__('ClusterIntegration|Install'); +      } else if (this.status === APPLICATION_SCHEDULED || this.status === APPLICATION_INSTALLING) { +        label = s__('ClusterIntegration|Installing'); +      } else if (this.status === APPLICATION_INSTALLED) { +        label = s__('ClusterIntegration|Installed'); +      } + +      return label; +    }, +    hasError() { +      return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE; +    }, +    generalErrorDescription() { +      return sprintf( +        s__('ClusterIntegration|Something went wrong while installing %{title}'), { +          title: this.title, +        }, +      ); +    }, +  }, +  methods: { +    installClicked() { +      eventHub.$emit('installApplication', this.id); +    }, +  }, +}; +</script> + +<template> +  <div +    class="gl-responsive-table-row gl-responsive-table-row-col-span" +    :class="rowJsClass" +  > +    <div +      class="gl-responsive-table-row-layout" +      role="row" +    > +      <a +        v-if="titleLink" +        :href="titleLink" +        target="blank" +        rel="noopener noreferrer" +        role="gridcell" +        class="table-section section-15 section-align-top js-cluster-application-title" +      > +        {{ title }} +      </a> +      <span +        v-else +        class="table-section section-15 section-align-top js-cluster-application-title" +      > +        {{ title }} +      </span> +      <div +        class="table-section section-wrap" +        role="gridcell" +      > +        <div v-html="description"></div> +      </div> +      <div +        class="table-section table-button-footer section-15 section-align-top" +        role="gridcell" +      > +        <div class="btn-group table-action-buttons"> +          <loading-button +            class="js-cluster-application-install-button" +            :loading="installButtonLoading" +            :disabled="installButtonDisabled" +            :label="installButtonLabel" +            @click="installClicked" +          /> +        </div> +      </div> +    </div> +    <div +      v-if="hasError" +      class="gl-responsive-table-row-layout" +      role="row" +    > +      <div +        class="alert alert-danger alert-block append-bottom-0 table-section section-100" +        role="gridcell" +      > +        <div> +          <p class="js-cluster-application-general-error-message"> +            {{ generalErrorDescription }} +          </p> +          <ul v-if="statusReason || requestReason"> +            <li +              v-if="statusReason" +              class="js-cluster-application-status-error-message" +            > +              {{ statusReason }} +            </li> +            <li +              v-if="requestReason" +              class="js-cluster-application-request-error-message" +            > +              {{ requestReason }} +            </li> +          </ul> +        </div> +      </div> +    </div> +  </div> +</template> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue new file mode 100644 index 00000000000..e5ae439d26e --- /dev/null +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -0,0 +1,114 @@ +<script> +import _ from 'underscore'; +import { s__, sprintf } from '../../locale'; +import applicationRow from './application_row.vue'; + +export default { +  props: { +    applications: { +      type: Object, +      required: false, +      default: () => ({}), +    }, +    helpPath: { +      type: String, +      required: false, +    }, +  }, +  components: { +    applicationRow, +  }, +  computed: { +    generalApplicationDescription() { +      return sprintf( +        _.escape(s__('ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}')), { +          helpLink: `<a href="${this.helpPath}"> +            ${_.escape(s__('ClusterIntegration|installing applications'))} +          </a>`, +        }, +        false, +      ); +    }, +    helmTillerDescription() { +      return _.escape(s__( +        `ClusterIntegration|Helm streamlines installing and managing Kubernets applications. +        Tiller runs inside of your Kubernetes Cluster, and manages +        releases of your charts.`, +      )); +    }, +    ingressDescription() { +      const descriptionParagraph = _.escape(s__( +        `ClusterIntegration|Ingress gives you a way to route requests to services based on the +        request host or path, centralizing a number of services into a single entrypoint.`, +      )); + +      const extraCostParagraph = sprintf( +        _.escape(s__('ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}')), { +          boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, +          pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> +            ${_.escape(s__('ClusterIntegration|GKE pricing'))} +          </a>`, +        }, +        false, +      ); + +      return ` +        <p> +          ${descriptionParagraph} +        </p> +        <p class="append-bottom-0"> +          ${extraCostParagraph} +        </p> +      `; +    }, +    gitlabRunnerDescription() { +      return _.escape(s__( +        `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs +        and send the results back to GitLab.`, +      )); +    }, +  }, +}; +</script> + +<template> +  <section class="settings no-animate expanded"> +    <div class="settings-header"> +      <h4> +        {{ s__('ClusterIntegration|Applications') }} +      </h4> +      <p +        class="append-bottom-0" +        v-html="generalApplicationDescription" +      > +      </p> +    </div> + +    <div class="settings-content"> +      <div class="append-bottom-20"> +        <application-row +          id="helm" +          :title="applications.helm.title" +          title-link="https://docs.helm.sh/" +          :description="helmTillerDescription" +          :status="applications.helm.status" +          :status-reason="applications.helm.statusReason" +          :request-status="applications.helm.requestStatus" +          :request-reason="applications.helm.requestReason" +        /> +        <application-row +         id="ingress" +         :title="applications.ingress.title" +         title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" +         :description="ingressDescription" +         :status="applications.ingress.status" +         :status-reason="applications.ingress.statusReason" +         :request-status="applications.ingress.requestStatus" +         :request-reason="applications.ingress.requestReason" +       /> +        <!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests --> +        <!-- Add GitLab Runner row, all other plumbing is complete --> +      </div> +    </div> +  </section> +</template> diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js new file mode 100644 index 00000000000..93223aefff8 --- /dev/null +++ b/app/assets/javascripts/clusters/constants.js @@ -0,0 +1,12 @@ +// These need to match what is returned from the server +export const APPLICATION_NOT_INSTALLABLE = 'not_installable'; +export const APPLICATION_INSTALLABLE = 'installable'; +export const APPLICATION_SCHEDULED = 'scheduled'; +export const APPLICATION_INSTALLING = 'installing'; +export const APPLICATION_INSTALLED = 'installed'; +export const APPLICATION_ERROR = 'errored'; + +// These are only used client-side +export const REQUEST_LOADING = 'request-loading'; +export const REQUEST_SUCCESS = 'request-success'; +export const REQUEST_FAILURE = 'request-failure'; diff --git a/app/assets/javascripts/clusters/event_hub.js b/app/assets/javascripts/clusters/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/clusters/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js new file mode 100644 index 00000000000..0ac8e68187d --- /dev/null +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -0,0 +1,24 @@ +import axios from 'axios'; +import setAxiosCsrfToken from '../../lib/utils/axios_utils'; + +export default class ClusterService { +  constructor(options = {}) { +    setAxiosCsrfToken(); + +    this.options = options; +    this.appInstallEndpointMap = { +      helm: this.options.installHelmEndpoint, +      ingress: this.options.installIngressEndpoint, +      runner: this.options.installRunnerEndpoint, +    }; +  } + +  fetchData() { +    return axios.get(this.options.endpoint); +  } + +  installApplication(appId) { +    const endpoint = this.appInstallEndpointMap[appId]; +    return axios.post(endpoint); +  } +} diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js new file mode 100644 index 00000000000..e731cdc3042 --- /dev/null +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -0,0 +1,68 @@ +import { s__ } from '../../locale'; + +export default class ClusterStore { +  constructor() { +    this.state = { +      helpPath: null, +      status: null, +      statusReason: null, +      applications: { +        helm: { +          title: s__('ClusterIntegration|Helm Tiller'), +          status: null, +          statusReason: null, +          requestStatus: null, +          requestReason: null, +        }, +        ingress: { +          title: s__('ClusterIntegration|Ingress'), +          status: null, +          statusReason: null, +          requestStatus: null, +          requestReason: null, +        }, +        runner: { +          title: s__('ClusterIntegration|GitLab Runner'), +          status: null, +          statusReason: null, +          requestStatus: null, +          requestReason: null, +        }, +      }, +    }; +  } + +  setHelpPath(helpPath) { +    this.state.helpPath = helpPath; +  } + +  updateStatus(status) { +    this.state.status = status; +  } + +  updateStatusReason(reason) { +    this.state.statusReason = reason; +  } + +  updateAppProperty(appId, prop, value) { +    this.state.applications[appId][prop] = value; +  } + +  updateStateFromServer(serverState = {}) { +    this.state.status = serverState.status; +    this.state.statusReason = serverState.status_reason; +    serverState.applications.forEach((serverAppEntry) => { +      const { +        name: appId, +        status, +        status_reason: statusReason, +      } = serverAppEntry; + +      this.state.applications[appId] = { +        ...(this.state.applications[appId] || {}), +        status, +        statusReason, +      }; +    }); +  } +} diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 0661087a1ba..e9a0dbaa59d 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -25,6 +25,11 @@          type: String,          required: true,        }, +      viewType: { +        type: String, +        required: false, +        default: 'child', +      },      },      mixins: [        pipelinesMixin, @@ -110,6 +115,7 @@          :pipelines="state.pipelines"          :update-graph-dropdown="updateGraphDropdown"          :auto-devops-help-path="autoDevopsHelpPath" +        :view-type="viewType"          />      </div>    </div> diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 997550b37fb..46b68ebe158 100644 --- a/app/assets/javascripts/new_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -2,7 +2,7 @@ import Cookies from 'js-cookie';  import _ from 'underscore';  import bp from './breakpoints'; -export default class NewNavSidebar { +export default class ContextualSidebar {    constructor() {      this.initDomElements();      this.render(); @@ -55,7 +55,7 @@ export default class NewNavSidebar {        this.$sidebar.toggleClass('sidebar-icons-only', collapsed);        this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);      } -    NewNavSidebar.setCollapsedCookie(collapsed); +    ContextualSidebar.setCollapsedCookie(collapsed);      this.toggleSidebarOverflow();    } diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index ff2f2c81971..bf40eb3ee11 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -1,5 +1,5 @@  /* eslint-disable no-new */ -/* global Flash */ +import Flash from './flash';  import DropLab from './droplab/drop_lab';  import ISetter from './droplab/plugins/input_setter'; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index cdf5e3c0290..49bb6c52180 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -1,7 +1,6 @@ -/* global Flash */ -  import Vue from 'vue';  import Cookies from 'js-cookie'; +import Flash from '../flash';  import Translate from '../vue_shared/translate';  import banner from './components/banner.vue';  import stageCodeComponent from './components/stage_code_component.vue'; diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index a663e30dfd0..54e13b79a4f 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -1,5 +1,5 @@  <script> -  /* global Flash */ +  import Flash from '../../flash';    import eventHub from '../eventhub';    import DeployKeysService from '../service';    import DeployKeysStore from '../store'; diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 6c78662baa7..c8874e48c09 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -1,5 +1,3 @@ -/* eslint-disable class-methods-use-this */ -  import './lib/utils/url_utility';  import FilesCommentButton from './files_comment_button';  import SingleFileDiff from './single_file_diff'; @@ -8,7 +6,7 @@ import imageDiffHelper from './image_diff/helpers/index';  const UNFOLD_COUNT = 20;  let isBound = false; -class Diff { +export default class Diff {    constructor() {      const $diffFile = $('.files .diff-file'); @@ -104,7 +102,7 @@ class Diff {      }      this.highlightSelectedLine();    } - +  // eslint-disable-next-line class-methods-use-this    handleParallelLineDown(e) {      const line = $(e.currentTarget);      const table = line.closest('table'); @@ -116,11 +114,11 @@ class Diff {        table.addClass(`${lineClass}-selected`);      }    } - +  // eslint-disable-next-line class-methods-use-this    diffViewType() {      return $('.inline-parallel-buttons a.active').data('view-type');    } - +  // eslint-disable-next-line class-methods-use-this    lineNumbers(line) {      const children = line.find('.diff-line-num').toArray();      if (children.length !== 2) { @@ -128,7 +126,7 @@ class Diff {      }      return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0);    } - +  // eslint-disable-next-line class-methods-use-this    highlightSelectedLine() {      const hash = gl.utils.getLocationHash();      const $diffFiles = $('.diff-file'); @@ -141,6 +139,3 @@ class Diff {      }    }  } - -window.gl = window.gl || {}; -window.gl.Diff = Diff; diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index efb6ced9f46..20ddcbfb8bd 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -1,9 +1,9 @@  /* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, max-len */  /* global CommentsStore */  /* global ResolveService */ -/* global Flash */  import Vue from 'vue'; +import Flash from '../../flash';  const ResolveBtn = Vue.extend({    props: { diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js index 2f063f6fe1f..6eae54f830b 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js +++ b/app/assets/javascripts/diff_notes/services/resolve.js @@ -1,7 +1,7 @@ -/* global Flash */  /* global CommentsStore */  import Vue from 'vue'; +import Flash from '../../flash';  import '../../vue_shared/vue_resource_interceptor';  window.gl = window.gl || {}; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 1edd460f380..44606989395 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,22 +1,23 @@  /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ +import { s__ } from './locale';  /* global ProjectSelect */ -/* global ShortcutsNavigation */ -/* global IssuableIndex */ -/* global ShortcutsIssuable */ +import IssuableIndex from './issuable_index';  /* global Milestone */ -/* global IssuableForm */ -/* global LabelsSelect */ +import IssuableForm from './issuable_form'; +import LabelsSelect from './labels_select';  /* global MilestoneSelect */  /* global NewBranchForm */  /* global NotificationsForm */  /* global NotificationsDropdown */ -/* global GroupAvatar */ +import groupAvatar from './group_avatar'; +import GroupLabelSubscription from './group_label_subscription';  /* global LineHighlighter */ -/* global BuildArtifacts */ -/* global GroupsSelect */ +import BuildArtifacts from './build_artifacts'; +import CILintEditor from './ci_lint_editor'; +import groupsSelect from './groups_select';  /* global Search */  /* global Admin */ -/* global NamespaceSelects */ +import NamespaceSelect from './namespace_select';  /* global NewCommitForm */  /* global NewBranchForm */  /* global Project */ @@ -28,12 +29,11 @@  /* global ProjectNew */  /* global ProjectShow */  /* global ProjectImport */ -/* global Labels */ -/* global Shortcuts */ -/* global ShortcutsFindFile */ +import Labels from './labels'; +import LabelManager from './label_manager';  /* global Sidebar */ -/* global ShortcutsWiki */ +import Flash from './flash';  import CommitsList from './commits';  import Issue from './issue';  import BindInOut from './behaviors/bind_in_out'; @@ -68,6 +68,7 @@ import initSettingsPanels from './settings_panels';  import initExperimentalFlags from './experimental_flags';  import OAuthRememberMe from './oauth_remember_me';  import PerformanceBar from './performance_bar'; +import initBroadcastMessagesForm from './broadcast_message';  import initNotes from './init_notes';  import initLegacyFilters from './init_legacy_filters';  import initIssuableSidebar from './init_issuable_sidebar'; @@ -75,9 +76,21 @@ import initProjectVisibilitySelector from './project_visibility';  import GpgBadges from './gpg_badges';  import UserFeatureHelper from './helpers/user_feature_helper';  import initChangesDropdown from './init_changes_dropdown'; +import NewGroupChild from './groups/new_group_child';  import AbuseReports from './abuse_reports';  import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';  import AjaxLoadingSpinner from './ajax_loading_spinner'; +import GlFieldErrors from './gl_field_errors'; +import GLForm from './gl_form'; +import Shortcuts from './shortcuts'; +import ShortcutsNavigation from './shortcuts_navigation'; +import ShortcutsFindFile from './shortcuts_find_file'; +import ShortcutsIssuable from './shortcuts_issuable'; +import U2FAuthenticate from './u2f/authenticate'; +import Members from './members'; +import memberExpirationDate from './member_expiration_date'; +import DueDateSelectors from './due_date_select'; +import Diff from './diff';  (function() {    var Dispatcher; @@ -90,8 +103,8 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';      }      Dispatcher.prototype.initPageScripts = function() { -      var page, path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl; -      page = $('body').attr('data-page'); +      var path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl; +      const page = $('body').attr('data-page');        if (!page) {          return false;        } @@ -161,11 +174,8 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';              const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');              filteredSearchManager.setup();            } -          if (page === 'projects:merge_requests:index') { -            new UserCallout({ setCalloutPerProject: true }); -          }            const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'; -          IssuableIndex.init(pagePrefix); +          new IssuableIndex(pagePrefix);            shortcut_handler = new ShortcutsNavigation();            new UsersSelect(); @@ -223,16 +233,21 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';          case 'projects:milestones:new':          case 'projects:milestones:edit':          case 'projects:milestones:update': +          new ZenMode(); +          new DueDateSelectors(); +          new GLForm($('.milestone-form'), true); +          break;          case 'groups:milestones:new':          case 'groups:milestones:edit':          case 'groups:milestones:update':            new ZenMode(); -          new gl.DueDateSelectors(); -          new gl.GLForm($('.milestone-form'), true); +          new DueDateSelectors(); +          new GLForm($('.milestone-form'), false);            break;          case 'projects:compare:show': -          new gl.Diff(); -          initChangesDropdown(); +          new Diff(); +          const paddingTop = 16; +          initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);            break;          case 'projects:branches:new':          case 'projects:branches:create': @@ -245,7 +260,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';          case 'projects:issues:new':          case 'projects:issues:edit':            shortcut_handler = new ShortcutsNavigation(); -          new gl.GLForm($('.issue-form'), true); +          new GLForm($('.issue-form'), true);            new IssuableForm($('.issue-form'));            new LabelsSelect();            new MilestoneSelect(); @@ -267,9 +282,9 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';            }          case 'projects:merge_requests:creations:diffs':          case 'projects:merge_requests:edit': -          new gl.Diff(); +          new Diff();            shortcut_handler = new ShortcutsNavigation(); -          new gl.GLForm($('.merge-request-form'), true); +          new GLForm($('.merge-request-form'), true);            new IssuableForm($('.merge-request-form'));            new LabelsSelect();            new MilestoneSelect(); @@ -278,7 +293,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';            break;          case 'projects:tags:new':            new ZenMode(); -          new gl.GLForm($('.tag-form'), true); +          new GLForm($('.tag-form'), true);            new RefSelectDropdown($('.js-branch-select'));            break;          case 'projects:snippets:show': @@ -288,20 +303,20 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';          case 'projects:snippets:edit':          case 'projects:snippets:create':          case 'projects:snippets:update': -          new gl.GLForm($('.snippet-form'), true); +          new GLForm($('.snippet-form'), true);            break;          case 'snippets:new':          case 'snippets:edit':          case 'snippets:create':          case 'snippets:update': -          new gl.GLForm($('.snippet-form'), false); +          new GLForm($('.snippet-form'), false);            break;          case 'projects:releases:edit':            new ZenMode(); -          new gl.GLForm($('.release-form'), true); +          new GLForm($('.release-form'), true);            break;          case 'projects:merge_requests:show': -          new gl.Diff(); +          new Diff();            shortcut_handler = new ShortcutsIssuable(true);            new ZenMode(); @@ -317,7 +332,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';            new gl.Activities();            break;          case 'projects:commit:show': -          new gl.Diff(); +          new Diff();            new ZenMode();            shortcut_handler = new ShortcutsNavigation();            new MiniPipelineGraph({ @@ -345,7 +360,10 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';          case 'projects:show':            shortcut_handler = new ShortcutsNavigation();            new NotificationsForm(); -          new UserCallout({ setCalloutPerProject: true }); +          new UserCallout({ +            setCalloutPerProject: true, +            className: 'js-autodevops-banner', +          });            if ($('#tree-slider').length) new TreeView();            if ($('.blob-viewer').length) new BlobViewer(); @@ -365,9 +383,6 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';          case 'projects:pipelines:new':            new NewBranchForm($('.js-new-pipeline-form'));            break; -        case 'projects:pipelines:index': -          new UserCallout({ setCalloutPerProject: true }); -          break;          case 'projects:pipelines:builds':          case 'projects:pipelines:failures':          case 'projects:pipelines:show': @@ -388,21 +403,26 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';            new gl.Activities();            break;          case 'groups:show': +          const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');            shortcut_handler = new ShortcutsNavigation();            new NotificationsForm();            new NotificationsDropdown();            new ProjectsList(); + +          if (newGroupChildWrapper) { +            new NewGroupChild(newGroupChildWrapper); +          }            break;          case 'groups:group_members:index': -          new gl.MemberExpirationDate(); -          new gl.Members(); +          memberExpirationDate(); +          new Members();            new UsersSelect();            break;          case 'projects:project_members:index': -          new gl.MemberExpirationDate('.js-access-expiration-date-groups'); -          new GroupsSelect(); -          new gl.MemberExpirationDate(); -          new gl.Members(); +          memberExpirationDate('.js-access-expiration-date-groups'); +          groupsSelect(); +          memberExpirationDate(); +          new Members();            new UsersSelect();            break;          case 'groups:new': @@ -411,11 +431,11 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';          case 'admin:groups:create':            BindInOut.initAll();            new Group(); -          new GroupAvatar(); +          groupAvatar();            break;          case 'groups:edit':          case 'admin:groups:edit': -          new GroupAvatar(); +          groupAvatar();            break;          case 'projects:tree:show':            shortcut_handler = new ShortcutsNavigation(); @@ -425,7 +445,6 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';            new TreeView();            new BlobViewer();            new NewCommitForm($('.js-create-dir-form')); -          new UserCallout({ setCalloutPerProject: true });            $('#tree-slider').waitForImages(function() {              ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);            }); @@ -457,13 +476,13 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';          case 'groups:labels:index':          case 'projects:labels:index':            if ($('.prioritized-labels').length) { -            new gl.LabelManager(); +            new LabelManager();            }            $('.label-subscription').each((i, el) => {              const $el = $(el);              if ($el.find('.dropdown-group-label').length) { -              new gl.GroupLabelSubscription($el); +              new GroupLabelSubscription($el);              } else {                new gl.ProjectLabelSubscription($el);              } @@ -505,7 +524,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';            break;          case 'ci:lints:create':          case 'ci:lints:show': -          new gl.CILintEditor(); +          new CILintEditor();            break;          case 'users:show':            new UserCallout(); @@ -523,29 +542,37 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';            break;          case 'profiles:personal_access_tokens:index':          case 'admin:impersonation_tokens:index': -          new gl.DueDateSelectors(); +          new DueDateSelectors();            break;          case 'projects:clusters:show': -          import(/* webpackChunkName: "clusters" */ './clusters') +          import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle')              .then(cluster => new cluster.default()) // eslint-disable-line new-cap -            .catch(() => {}); +            .catch((err) => { +              Flash(s__('ClusterIntegration|Problem setting up the cluster JavaScript')); +              throw err; +            });            break;        }        switch (path[0]) {          case 'sessions':          case 'omniauth_callbacks':            if (!gon.u2f) break; -          gl.u2fAuthenticate = new gl.U2FAuthenticate( +          const u2fAuthenticate = new U2FAuthenticate(              $('#js-authenticate-u2f'),              '#js-login-u2f-form',              gon.u2f,              document.querySelector('#js-login-2fa-device'),              document.querySelector('.js-2fa-form'),            ); -          gl.u2fAuthenticate.start(); +          u2fAuthenticate.start(); +          // needed in rspec +          gl.u2fAuthenticate = u2fAuthenticate;          case 'admin':            new Admin();            switch (path[1]) { +            case 'broadcast_messages': +              initBroadcastMessagesForm(); +              break;              case 'cohorts':                new UsagePing();                break; @@ -553,7 +580,8 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';                new UsersSelect();                break;              case 'projects': -              new NamespaceSelects(); +              document.querySelectorAll('.js-namespace-select') +                .forEach(dropdown => new NamespaceSelect({ dropdown }));                break;              case 'labels':                switch (path[2]) { @@ -602,7 +630,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';                new Wikis();                shortcut_handler = new ShortcutsWiki();                new ZenMode(); -              new gl.GLForm($('.wiki-form'), true); +              new GLForm($('.wiki-form'), true);                break;              case 'snippets':                shortcut_handler = new ShortcutsNavigation(); @@ -627,12 +655,6 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';                shortcut_handler = new ShortcutsNavigation();            }            break; -        case 'users': -          const action = path[1]; -          import(/* webpackChunkName: 'user_profile' */ './users') -            .then(user => user.default(action)) -            .catch(() => {}); -          break;        }        // If we haven't installed a custom shortcut handler, install the default one        if (!shortcut_handler) { @@ -653,7 +675,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';      Dispatcher.prototype.initFieldErrors = function() {        $('.gl-show-field-errors').each((i, form) => { -        new gl.GlFieldErrors(form); +        new GlFieldErrors(form);        });      }; diff --git a/app/assets/javascripts/droplab/plugins/filter.js b/app/assets/javascripts/droplab/plugins/filter.js index d6a1aadd49c..404d707cf7a 100644 --- a/app/assets/javascripts/droplab/plugins/filter.js +++ b/app/assets/javascripts/droplab/plugins/filter.js @@ -79,8 +79,6 @@ const Filter = {      this.hook.trigger.addEventListener('keydown.dl', this.eventWrapper.debounceKeydown);      this.hook.trigger.addEventListener('mousedown.dl', this.eventWrapper.debounceKeydown); - -    this.debounceKeydown({ detail: { hook: this.hook } });    },    destroy: function destroy() { diff --git a/app/assets/javascripts/droplab/utils.js b/app/assets/javascripts/droplab/utils.js index 4da7344604e..bfe056a0fcc 100644 --- a/app/assets/javascripts/droplab/utils.js +++ b/app/assets/javascripts/droplab/utils.js @@ -30,7 +30,7 @@ const utils = {    },    isDropDownParts(target) { -    if (!target || target.tagName === 'HTML') return false; +    if (!target || !target.hasAttribute || target.tagName === 'HTML') return false;      return target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN);    },  }; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 1cba65d17cd..b7747ee3f83 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,305 +1,274 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */ -/* global Dropzone */ +import Dropzone from 'dropzone';  import _ from 'underscore';  import './preview_markdown';  import csrf from './lib/utils/csrf'; -window.DropzoneInput = (function() { -  function DropzoneInput(form) { -    const divHover = '<div class="div-dropzone-hover"></div>'; -    const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; -    const $attachButton = form.find('.button-attach-file'); -    const $attachingFileMessage = form.find('.attaching-file-message'); -    const $cancelButton = form.find('.button-cancel-uploading-files'); -    const $retryLink = form.find('.retry-uploading-link'); -    const $uploadProgress = form.find('.uploading-progress'); -    const $uploadingErrorContainer = form.find('.uploading-error-container'); -    const $uploadingErrorMessage = form.find('.uploading-error-message'); -    const $uploadingProgressContainer = form.find('.uploading-progress-container'); -    const uploadsPath = window.uploads_path || null; -    const maxFileSize = gon.max_file_size || 10; -    const formTextarea = form.find('.js-gfm-input'); -    let handlePaste; -    let pasteText; -    let addFileToForm; -    let updateAttachingMessage; -    let isImage; -    let getFilename; -    let uploadFile; - -    formTextarea.wrap('<div class="div-dropzone"></div>'); -    formTextarea.on('paste', (function(_this) { -      return function(event) { -        return handlePaste(event); -      }; -    })(this)); - -    // Add dropzone area to the form. -    const $mdArea = formTextarea.closest('.md-area'); -    form.setupMarkdownPreview(); -    const $formDropzone = form.find('.div-dropzone'); -    $formDropzone.parent().addClass('div-dropzone-wrapper'); -    $formDropzone.append(divHover); -    $formDropzone.find('.div-dropzone-hover').append(iconPaperclip); - -    if (!uploadsPath) return; - -    const dropzone = $formDropzone.dropzone({ -      url: uploadsPath, -      dictDefaultMessage: '', -      clickable: true, -      paramName: 'file', -      maxFilesize: maxFileSize, -      uploadMultiple: false, -      headers: csrf.headers, -      previewContainer: false, -      processing: function() { -        return $('.div-dropzone-alert').alert('close'); -      }, -      dragover: function() { -        $mdArea.addClass('is-dropzone-hover'); -        form.find('.div-dropzone-hover').css('opacity', 0.7); -      }, -      dragleave: function() { -        $mdArea.removeClass('is-dropzone-hover'); -        form.find('.div-dropzone-hover').css('opacity', 0); -      }, -      drop: function() { -        $mdArea.removeClass('is-dropzone-hover'); -        form.find('.div-dropzone-hover').css('opacity', 0); -        formTextarea.focus(); -      }, -      success: function(header, response) { -        const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length; -        const shouldPad = processingFileCount >= 1; - -        pasteText(response.link.markdown, shouldPad); -        // Show 'Attach a file' link only when all files have been uploaded. -        if (!processingFileCount) $attachButton.removeClass('hide'); -        addFileToForm(response.link.url); -      }, -      error: function(file, errorMessage = 'Attaching the file failed.', xhr) { -        // If 'error' event is fired by dropzone, the second parameter is error message. -        // If the 'errorMessage' parameter is empty, the default error message is set. -        // If the 'error' event is fired by backend (xhr) error response, the third parameter is -        // xhr object (xhr.responseText is error message). -        // On error we hide the 'Attach' and 'Cancel' buttons -        // and show an error. - -        // If there's xhr error message, let's show it instead of dropzone's one. -        const message = xhr ? xhr.responseText : errorMessage; - -        $uploadingErrorContainer.removeClass('hide'); -        $uploadingErrorMessage.html(message); -        $attachButton.addClass('hide'); -        $cancelButton.addClass('hide'); -      }, -      totaluploadprogress: function(totalUploadProgress) { -        updateAttachingMessage(this.files, $attachingFileMessage); -        $uploadProgress.text(Math.round(totalUploadProgress) + '%'); -      }, -      sending: function(file) { -        // DOM elements already exist. -        // Instead of dynamically generating them, -        // we just either hide or show them. -        $attachButton.addClass('hide'); -        $uploadingErrorContainer.addClass('hide'); -        $uploadingProgressContainer.removeClass('hide'); -        $cancelButton.removeClass('hide'); -      }, -      removedfile: function() { -        $attachButton.removeClass('hide'); -        $cancelButton.addClass('hide'); -        $uploadingProgressContainer.addClass('hide'); -        $uploadingErrorContainer.addClass('hide'); -      }, -      queuecomplete: function() { -        $('.dz-preview').remove(); -        $('.markdown-area').trigger('input'); +export default function dropzoneInput(form) { +  const divHover = '<div class="div-dropzone-hover"></div>'; +  const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; +  const $attachButton = form.find('.button-attach-file'); +  const $attachingFileMessage = form.find('.attaching-file-message'); +  const $cancelButton = form.find('.button-cancel-uploading-files'); +  const $retryLink = form.find('.retry-uploading-link'); +  const $uploadProgress = form.find('.uploading-progress'); +  const $uploadingErrorContainer = form.find('.uploading-error-container'); +  const $uploadingErrorMessage = form.find('.uploading-error-message'); +  const $uploadingProgressContainer = form.find('.uploading-progress-container'); +  const uploadsPath = window.uploads_path || null; +  const maxFileSize = gon.max_file_size || 10; +  const formTextarea = form.find('.js-gfm-input'); +  let handlePaste; +  let pasteText; +  let addFileToForm; +  let updateAttachingMessage; +  let isImage; +  let getFilename; +  let uploadFile; + +  formTextarea.wrap('<div class="div-dropzone"></div>'); +  formTextarea.on('paste', event => handlePaste(event)); + +  // Add dropzone area to the form. +  const $mdArea = formTextarea.closest('.md-area'); +  form.setupMarkdownPreview(); +  const $formDropzone = form.find('.div-dropzone'); +  $formDropzone.parent().addClass('div-dropzone-wrapper'); +  $formDropzone.append(divHover); +  $formDropzone.find('.div-dropzone-hover').append(iconPaperclip); + +  if (!uploadsPath) return; + +  const dropzone = $formDropzone.dropzone({ +    url: uploadsPath, +    dictDefaultMessage: '', +    clickable: true, +    paramName: 'file', +    maxFilesize: maxFileSize, +    uploadMultiple: false, +    headers: csrf.headers, +    previewContainer: false, +    processing: () => $('.div-dropzone-alert').alert('close'), +    dragover: () => { +      $mdArea.addClass('is-dropzone-hover'); +      form.find('.div-dropzone-hover').css('opacity', 0.7); +    }, +    dragleave: () => { +      $mdArea.removeClass('is-dropzone-hover'); +      form.find('.div-dropzone-hover').css('opacity', 0); +    }, +    drop: () => { +      $mdArea.removeClass('is-dropzone-hover'); +      form.find('.div-dropzone-hover').css('opacity', 0); +      formTextarea.focus(); +    }, +    success(header, response) { +      const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length; +      const shouldPad = processingFileCount >= 1; + +      pasteText(response.link.markdown, shouldPad); +      // Show 'Attach a file' link only when all files have been uploaded. +      if (!processingFileCount) $attachButton.removeClass('hide'); +      addFileToForm(response.link.url); +    }, +    error: (file, errorMessage = 'Attaching the file failed.', xhr) => { +      // If 'error' event is fired by dropzone, the second parameter is error message. +      // If the 'errorMessage' parameter is empty, the default error message is set. +      // If the 'error' event is fired by backend (xhr) error response, the third parameter is +      // xhr object (xhr.responseText is error message). +      // On error we hide the 'Attach' and 'Cancel' buttons +      // and show an error. + +      // If there's xhr error message, let's show it instead of dropzone's one. +      const message = xhr ? xhr.responseText : errorMessage; -        $uploadingProgressContainer.addClass('hide'); -        $cancelButton.addClass('hide'); +      $uploadingErrorContainer.removeClass('hide'); +      $uploadingErrorMessage.html(message); +      $attachButton.addClass('hide'); +      $cancelButton.addClass('hide'); +    }, +    totaluploadprogress(totalUploadProgress) { +      updateAttachingMessage(this.files, $attachingFileMessage); +      $uploadProgress.text(`${Math.round(totalUploadProgress)}%`); +    }, +    sending: () => { +      // DOM elements already exist. +      // Instead of dynamically generating them, +      // we just either hide or show them. +      $attachButton.addClass('hide'); +      $uploadingErrorContainer.addClass('hide'); +      $uploadingProgressContainer.removeClass('hide'); +      $cancelButton.removeClass('hide'); +    }, +    removedfile: () => { +      $attachButton.removeClass('hide'); +      $cancelButton.addClass('hide'); +      $uploadingProgressContainer.addClass('hide'); +      $uploadingErrorContainer.addClass('hide'); +    }, +    queuecomplete: () => { +      $('.dz-preview').remove(); +      $('.markdown-area').trigger('input'); + +      $uploadingProgressContainer.addClass('hide'); +      $cancelButton.addClass('hide'); +    }, +  }); + +  const child = $(dropzone[0]).children('textarea'); + +  // removeAllFiles(true) stops uploading files (if any) +  // and remove them from dropzone files queue. +  $cancelButton.on('click', (e) => { +    e.preventDefault(); +    e.stopPropagation(); +    Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true); +  }); + +  // If 'error' event is fired, we store a failed files, +  // clear dropzone files queue, change status of failed files to undefined, +  // and add that files to the dropzone files queue again. +  // addFile() adds file to dropzone files queue and upload it. +  $retryLink.on('click', (e) => { +    const dropzoneInstance = Dropzone.forElement(e.target.closest('.js-main-target-form').querySelector('.div-dropzone')); +    const failedFiles = dropzoneInstance.files; + +    e.preventDefault(); + +    // 'true' parameter of removeAllFiles() cancels +    // uploading of files that are being uploaded at the moment. +    dropzoneInstance.removeAllFiles(true); + +    failedFiles.map((failedFile) => { +      const file = failedFile; + +      if (file.status === Dropzone.ERROR) { +        file.status = undefined; +        file.accepted = undefined;        } -    }); - -    const child = $(dropzone[0]).children('textarea'); - -    // removeAllFiles(true) stops uploading files (if any) -    // and remove them from dropzone files queue. -    $cancelButton.on('click', (e) => { -      const target = e.target.closest('.js-main-target-form').querySelector('.div-dropzone'); - -      e.preventDefault(); -      e.stopPropagation(); -      Dropzone.forElement(target).removeAllFiles(true); -    }); - -    // If 'error' event is fired, we store a failed files, -    // clear dropzone files queue, change status of failed files to undefined, -    // and add that files to the dropzone files queue again. -    // addFile() adds file to dropzone files queue and upload it. -    $retryLink.on('click', (e) => { -      const dropzoneInstance = Dropzone.forElement(e.target.closest('.js-main-target-form').querySelector('.div-dropzone')); -      const failedFiles = dropzoneInstance.files; -      e.preventDefault(); - -      // 'true' parameter of removeAllFiles() cancels uploading of files that are being uploaded at the moment. -      dropzoneInstance.removeAllFiles(true); - -      failedFiles.map((failedFile, i) => { -        const file = failedFile; - -        if (file.status === Dropzone.ERROR) { -          file.status = undefined; -          file.accepted = undefined; -        } - -        return dropzoneInstance.addFile(file); -      }); +      return dropzoneInstance.addFile(file);      }); - -    handlePaste = function(event) { -      var filename, image, pasteEvent, text; -      pasteEvent = event.originalEvent; -      if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { -        image = isImage(pasteEvent); -        if (image) { -          event.preventDefault(); -          filename = getFilename(pasteEvent) || 'image.png'; -          text = `{{${filename}}}`; -          pasteText(text); -          return uploadFile(image.getAsFile(), filename); -        } +  }); +  // eslint-disable-next-line consistent-return +  handlePaste = (event) => { +    const pasteEvent = event.originalEvent; +    if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { +      const image = isImage(pasteEvent); +      if (image) { +        event.preventDefault(); +        const filename = getFilename(pasteEvent) || 'image.png'; +        const text = `{{${filename}}}`; +        pasteText(text); +        return uploadFile(image.getAsFile(), filename);        } -    }; - -    isImage = function(data) { -      var i, item; -      i = 0; -      while (i < data.clipboardData.items.length) { -        item = data.clipboardData.items[i]; -        if (item.type.indexOf('image') !== -1) { -          return item; -        } -        i += 1; -      } -      return false; -    }; - -    pasteText = function(text, shouldPad) { -      var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; -      var formattedText = text; -      if (shouldPad) formattedText += "\n\n"; -      const textarea = child.get(0); -      caretStart = textarea.selectionStart; -      caretEnd = textarea.selectionEnd; -      textEnd = $(child).val().length; -      beforeSelection = $(child).val().substring(0, caretStart); -      afterSelection = $(child).val().substring(caretEnd, textEnd); -      $(child).val(beforeSelection + formattedText + afterSelection); -      textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); -      textarea.style.height = `${textarea.scrollHeight}px`; -      formTextarea.get(0).dispatchEvent(new Event('input')); -      return formTextarea.trigger('input'); -    }; - -    addFileToForm = function(path) { -      $(form).append('<input type="hidden" name="files[]" value="' + _.escape(path) + '">'); -    }; - -    getFilename = function(e) { -      var value; -      if (window.clipboardData && window.clipboardData.getData) { -        value = window.clipboardData.getData('Text'); -      } else if (e.clipboardData && e.clipboardData.getData) { -        value = e.clipboardData.getData('text/plain'); -      } -      value = value.split("\r"); -      return value[0]; -    }; - -    const showSpinner = function(e) { -      return $uploadingProgressContainer.removeClass('hide'); -    }; - -    const closeSpinner = function() { -      return $uploadingProgressContainer.addClass('hide'); -    }; - -    const showError = function(message) { -      $uploadingErrorContainer.removeClass('hide'); -      $uploadingErrorMessage.html(message); -    }; - -    const closeAlertMessage = function() { -      return form.find('.div-dropzone-alert').alert('close'); -    }; - -    const insertToTextArea = function(filename, url) { -      return $(child).val(function(index, val) { -        return val.replace(`{{${filename}}}`, url); -      }); -    }; - -    const appendToTextArea = function(url) { -      return $(child).val(function(index, val) { -        return val + url + "\n"; -      }); -    }; - -    uploadFile = function(item, filename) { -      var formData; -      formData = new FormData(); -      formData.append('file', item, filename); -      return $.ajax({ -        url: uploadsPath, -        type: 'POST', -        data: formData, -        dataType: 'json', -        processData: false, -        contentType: false, -        headers: csrf.headers, -        beforeSend: function() { -          showSpinner(); -          return closeAlertMessage(); -        }, -        success: function(e, textStatus, response) { -          return insertToTextArea(filename, response.responseJSON.link.markdown); -        }, -        error: function(response) { -          return showError(response.responseJSON.message); -        }, -        complete: function() { -          return closeSpinner(); -        } -      }); -    }; - -    updateAttachingMessage = (files, messageContainer) => { -      let attachingMessage; -      const filesCount = files.filter(function(file) { -        return file.status === 'uploading' || -               file.status === 'queued'; -      }).length; - -      // Dinamycally change uploading files text depending on files number in -      // dropzone files queue. -      if (filesCount > 1) { -        attachingMessage = 'Attaching ' + filesCount + ' files -'; -      } else { -        attachingMessage = 'Attaching a file -'; +    } +  }; + +  isImage = (data) => { +    let i = 0; +    while (i < data.clipboardData.items.length) { +      const item = data.clipboardData.items[i]; +      if (item.type.indexOf('image') !== -1) { +        return item;        } - -      messageContainer.text(attachingMessage); -    }; - -    form.find('.markdown-selector').click(function(e) { -      e.preventDefault(); -      $(this).closest('.gfm-form').find('.div-dropzone').click(); -      formTextarea.focus(); +      i += 1; +    } +    return false; +  }; + +  pasteText = (text, shouldPad) => { +    let formattedText = text; +    if (shouldPad) { +      formattedText += '\n\n'; +    } +    const textarea = child.get(0); +    const caretStart = textarea.selectionStart; +    const caretEnd = textarea.selectionEnd; +    const textEnd = $(child).val().length; +    const beforeSelection = $(child).val().substring(0, caretStart); +    const afterSelection = $(child).val().substring(caretEnd, textEnd); +    $(child).val(beforeSelection + formattedText + afterSelection); +    textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); +    textarea.style.height = `${textarea.scrollHeight}px`; +    formTextarea.get(0).dispatchEvent(new Event('input')); +    return formTextarea.trigger('input'); +  }; + +  addFileToForm = (path) => { +    $(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`); +  }; + +  getFilename = (e) => { +    let value; +    if (window.clipboardData && window.clipboardData.getData) { +      value = window.clipboardData.getData('Text'); +    } else if (e.clipboardData && e.clipboardData.getData) { +      value = e.clipboardData.getData('text/plain'); +    } +    value = value.split('\r'); +    return value[0]; +  }; + +  const showSpinner = () => $uploadingProgressContainer.removeClass('hide'); + +  const closeSpinner = () => $uploadingProgressContainer.addClass('hide'); + +  const showError = (message) => { +    $uploadingErrorContainer.removeClass('hide'); +    $uploadingErrorMessage.html(message); +  }; + +  const closeAlertMessage = () => form.find('.div-dropzone-alert').alert('close'); + +  const insertToTextArea = (filename, url) => { +    const $child = $(child); +    $child.val((index, val) => val.replace(`{{${filename}}}`, url)); + +    $child.trigger('change'); +  }; + +  uploadFile = (item, filename) => { +    const formData = new FormData(); +    formData.append('file', item, filename); +    return $.ajax({ +      url: uploadsPath, +      type: 'POST', +      data: formData, +      dataType: 'json', +      processData: false, +      contentType: false, +      headers: csrf.headers, +      beforeSend: () => { +        showSpinner(); +        return closeAlertMessage(); +      }, +      success: (e, text, response) => { +        const md = response.responseJSON.link.markdown; +        insertToTextArea(filename, md); +      }, +      error: response => showError(response.responseJSON.message), +      complete: () => closeSpinner(),      }); -  } - -  return DropzoneInput; -})(); +  }; + +  updateAttachingMessage = (files, messageContainer) => { +    let attachingMessage; +    const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued').length; + +    // Dinamycally change uploading files text depending on files number in +    // dropzone files queue. +    if (filesCount > 1) { +      attachingMessage = `Attaching ${filesCount} files -`; +    } else { +      attachingMessage = 'Attaching a file -'; +    } + +    messageContainer.text(attachingMessage); +  }; + +  form.find('.markdown-selector').click(function onMarkdownClick(e) { +    e.preventDefault(); +    $(this).closest('.gfm-form').find('.div-dropzone').click(); +    formTextarea.focus(); +  }); +} diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index ee71728184f..ada985913bb 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -1,8 +1,7 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */  /* global dateFormat */  import Pikaday from 'pikaday'; -import DateFix from './lib/utils/datefix'; +import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';  class DueDateSelect {    constructor({ $dropdown, $loading } = {}) { @@ -17,8 +16,8 @@ class DueDateSelect {      this.$value = $block.find('.value');      this.$valueContent = $block.find('.value-content');      this.$sidebarValue = $('.js-due-date-sidebar-value', $block); -    this.fieldName = $dropdown.data('field-name'), -    this.abilityName = $dropdown.data('ability-name'), +    this.fieldName = $dropdown.data('field-name'); +    this.abilityName = $dropdown.data('ability-name');      this.issueUpdateURL = $dropdown.data('issue-update');      this.rawSelectedDate = null; @@ -39,20 +38,20 @@ class DueDateSelect {        hidden: () => {          this.$selectbox.hide();          this.$value.css('display', ''); -      } +      },      });    }    initDatePicker() {      const $dueDateInput = $(`input[name='${this.fieldName}']`); -    const dateFix = DateFix.dashedFix($dueDateInput.val());      const calendar = new Pikaday({        field: $dueDateInput.get(0),        theme: 'gitlab-theme',        format: 'yyyy-mm-dd', +      parse: dateString => parsePikadayDate(dateString), +      toString: date => pikadayToString(date),        onSelect: (dateText) => { -        const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); -        $dueDateInput.val(formattedDate); +        $dueDateInput.val(calendar.toString(dateText));          if (this.$dropdown.hasClass('js-issue-boards-due-date')) {            gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val(); @@ -60,10 +59,10 @@ class DueDateSelect {          } else {            this.saveDueDate(true);          } -      } +      },      }); -    calendar.setDate(dateFix); +    calendar.setDate(parsePikadayDate($dueDateInput.val()));      this.$datePicker.append(calendar.el);      this.$datePicker.data('pikaday', calendar);    } @@ -79,8 +78,8 @@ class DueDateSelect {          gl.issueBoards.BoardsStore.detail.issue.dueDate = '';          this.updateIssueBoardIssue();        } else { -        $("input[name='" + this.fieldName + "']").val(''); -        return this.saveDueDate(false); +        $(`input[name='${this.fieldName}']`).val(''); +        this.saveDueDate(false);        }      });    } @@ -111,7 +110,7 @@ class DueDateSelect {      this.datePayload = datePayload;    } -  updateIssueBoardIssue () { +  updateIssueBoardIssue() {      this.$loading.fadeIn();      this.$dropdown.trigger('loading.gl.dropdown');      this.$selectbox.hide(); @@ -149,8 +148,8 @@ class DueDateSelect {          return selectedDateValue.length ?            $('.js-remove-due-date-holder').removeClass('hidden') :            $('.js-remove-due-date-holder').addClass('hidden'); -      } -    }).done((data) => { +      }, +    }).done(() => {        if (isDropdown) {          this.$dropdown.trigger('loaded.gl.dropdown');          this.$dropdown.dropdown('toggle'); @@ -160,27 +159,28 @@ class DueDateSelect {    }  } -class DueDateSelectors { +export default class DueDateSelectors {    constructor() {      this.initMilestoneDatePicker();      this.initIssuableSelect();    } - +  // eslint-disable-next-line class-methods-use-this    initMilestoneDatePicker() { -    $('.datepicker').each(function() { +    $('.datepicker').each(function initPikadayMilestone() {        const $datePicker = $(this); -      const dateFix = DateFix.dashedFix($datePicker.val());        const calendar = new Pikaday({          field: $datePicker.get(0),          theme: 'gitlab-theme animate-picker',          format: 'yyyy-mm-dd',          container: $datePicker.parent().get(0), +        parse: dateString => parsePikadayDate(dateString), +        toString: date => pikadayToString(date),          onSelect(dateText) { -          $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); -        } +          $datePicker.val(calendar.toString(dateText)); +        },        }); -      calendar.setDate(dateFix); +      calendar.setDate(parsePikadayDate($datePicker.val()));        $datePicker.data('pikaday', calendar);      }); @@ -191,19 +191,17 @@ class DueDateSelectors {        calendar.setDate(null);      });    } - +  // eslint-disable-next-line class-methods-use-this    initIssuableSelect() {      const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();      $('.js-due-date-select').each((i, dropdown) => {        const $dropdown = $(dropdown); +      // eslint-disable-next-line no-new        new DueDateSelect({          $dropdown, -        $loading +        $loading,        });      });    }  } - -window.gl = window.gl || {}; -window.gl.DueDateSelectors = DueDateSelectors; diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue index ce5f6219a3e..c039ae85cfb 100644 --- a/app/assets/javascripts/environments/components/environment.vue +++ b/app/assets/javascripts/environments/components/environment.vue @@ -1,6 +1,6 @@  <script> -/* global Flash */  import Visibility from 'visibilityjs'; +import Flash from '../../flash';  import EnvironmentsService from '../services/environments_service';  import environmentTable from './environments_table.vue';  import EnvironmentsStore from '../stores/environments_store'; diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 6de01fa53d0..fc0308b81ba 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -421,7 +421,11 @@ export default {  </script>  <template>    <div -    :class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }" +    class="gl-responsive-table-row" +    :class="{ +      'js-child-row environment-child-row': model.isChildren, +      'folder-row': model.isFolder, +    }"      role="row">      <div class="table-section section-10" role="gridcell">        <div @@ -495,15 +499,16 @@ export default {        </a>      </div> -    <div class="table-section section-25" role="gridcell"> +    <div +      v-if="!model.isFolder" +      class="table-section section-25" role="gridcell">        <div -        v-if="!model.isFolder"          role="rowheader"          class="table-mobile-header">          Commit        </div>        <div -        v-if="!model.isFolder && hasLastDeploymentKey" +        v-if="hasLastDeploymentKey"          class="js-commit-component table-mobile-content">          <commit-component            :tag="commitTag" @@ -514,21 +519,22 @@ export default {            :author="commitAuthor"/>        </div>        <div -        v-if="!model.isFolder && !hasLastDeploymentKey" +        v-if="!hasLastDeploymentKey"          class="commit-title table-mobile-content">          No deployments yet        </div>      </div> -    <div class="table-section section-10" role="gridcell"> +    <div +      v-if="!model.isFolder" +      class="table-section section-10" role="gridcell">        <div -        v-if="!model.isFolder"          role="rowheader"          class="table-mobile-header">          Updated        </div>        <span -        v-if="!model.isFolder && canShowDate" +        v-if="canShowDate"          class="environment-created-date-timeago table-mobile-content">          {{createdDate}}        </span> diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 01e70c0bbb7..b155560df9d 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -1,6 +1,6 @@  <script> -/* global Flash */  import Visibility from 'visibilityjs'; +import Flash from '../../flash';  import EnvironmentsService from '../services/environments_service';  import environmentTable from '../components/environments_table.vue';  import EnvironmentsStore from '../stores/environments_store'; diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index a00d29a845a..90020344748 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,6 +1,3 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ -/* global notes */ -  /* Developer beware! Do not add logic to showButton or hideButton   * that will force a reflow. Doing so will create a signficant performance   * bottleneck for pages with large diffs. For a comprehensive list of what @@ -20,8 +17,10 @@ const DIFF_EXPANDED_CLASS = 'diff-expanded';  export default {    init($diffFile) { -    /* Caching is used only when the following members are *true*. This is because there are likely to be -     * differently configured versions of diffs in the same session. However if these values are true, they +    /* Caching is used only when the following members are *true*. +     * This is because there are likely to be +     * differently configured versions of diffs in the same session. +     * However if these values are true, they       * will be true in all cases */      if (!this.userCanCreateNote) { diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 6d516a253bb..9e91f72b2ea 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -6,10 +6,11 @@ import _ from 'underscore';   */  export default class FilterableList { -  constructor(form, filter, holder) { +  constructor(form, filter, holder, filterInputField = 'filter_groups') {      this.filterForm = form;      this.listFilterElement = filter;      this.listHolderElement = holder; +    this.filterInputField = filterInputField;      this.isBusy = false;    } @@ -32,10 +33,10 @@ export default class FilterableList {    onFilterInput() {      const $form = $(this.filterForm);      const queryData = {}; -    const filterGroupsParam = $form.find('[name="filter_groups"]').val(); +    const filterGroupsParam = $form.find(`[name="${this.filterInputField}"]`).val();      if (filterGroupsParam) { -      queryData.filter_groups = filterGroupsParam; +      queryData[this.filterInputField] = filterGroupsParam;      }      this.filterResults(queryData); diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js index ada14d2053c..a6cc079d720 100644 --- a/app/assets/javascripts/filtered_search/dropdown_emoji.js +++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js @@ -1,7 +1,6 @@ -/* global Flash */ - -import Ajax from '~/droplab/plugins/ajax'; -import Filter from '~/droplab/plugins/filter'; +import Flash from '../flash'; +import Ajax from '../droplab/plugins/ajax'; +import Filter from '../droplab/plugins/filter';  import './filtered_search_dropdown';  class DropdownEmoji extends gl.FilteredSearchDropdown { diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index b32d589481d..788fb1dc614 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -1,7 +1,6 @@ -/* global Flash */ - -import Ajax from '~/droplab/plugins/ajax'; -import Filter from '~/droplab/plugins/filter'; +import Flash from '../flash'; +import Ajax from '../droplab/plugins/ajax'; +import Filter from '../droplab/plugins/filter';  import './filtered_search_dropdown';  class DropdownNonUser extends gl.FilteredSearchDropdown { diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index ce8817b1b2e..a9e2b65def0 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -1,6 +1,5 @@ -/* global Flash */ - -import AjaxFilter from '~/droplab/plugins/ajax_filter'; +import Flash from '../flash'; +import AjaxFilter from '../droplab/plugins/ajax_filter';  import './filtered_search_dropdown';  import { addClassIfElementExists } from '../lib/utils/dom_utils'; diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 8d711e3213c..cf8a9b0402b 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -147,6 +147,16 @@ class DropdownUtils {      return dataValue !== null;    } +  static getVisualTokenValues(visualToken) { +    const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim(); +    let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim(); +    if (tokenName === 'label' && tokenValue) { +      // remove leading symbol and wrapping quotes +      tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, ''); +    } +    return { tokenName, tokenValue }; +  } +    // Determines the full search query (visual tokens + input)    static getSearchQuery(untilInput = false) {      const container = FilteredSearchContainer.container; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index a44dc279a6f..69c57f923b6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,3 +1,4 @@ +import Flash from '../flash';  import FilteredSearchContainer from './container';  import RecentSearchesRoot from './recent_searches_root';  import RecentSearchesStore from './stores/recent_searches_store'; @@ -36,7 +37,7 @@ class FilteredSearchManager {        .catch((error) => {          if (error.name === 'RecentSearchesServiceError') return undefined;          // eslint-disable-next-line no-new -        new window.Flash('An error occurred while parsing recent searches'); +        new Flash('An error occurred while parsing recent searches');          // Gracefully fail to empty array          return [];        }) @@ -184,8 +185,8 @@ class FilteredSearchManager {      if (e.keyCode === 8 || e.keyCode === 46) {        const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); -      const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim(); -      const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName); +      const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken); +      const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);        if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {          this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();          gl.FilteredSearchVisualTokens.removeLastTokenPartial(); @@ -335,8 +336,8 @@ class FilteredSearchManager {        let canClearToken = t.classList.contains('js-visual-token');        if (canClearToken) { -        const tokenKey = t.querySelector('.name').textContent.trim(); -        canClearToken = this.canEdit && this.canEdit(tokenKey); +        const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t); +        canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue);        }        if (canClearToken) { @@ -468,7 +469,7 @@ class FilteredSearchManager {            }            hasFilteredSearch = true; -          const canEdit = this.canEdit && this.canEdit(sanitizedKey); +          const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);            gl.FilteredSearchVisualTokens.addFilterVisualToken(              sanitizedKey,              `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 28e8240169d..6139e81fe6d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,5 +1,5 @@  import AjaxCache from '../lib/utils/ajax_cache'; -import '../flash'; /* global Flash */ +import Flash from '../flash';  import FilteredSearchContainer from './container';  import UsersCache from '../lib/utils/users_cache'; @@ -38,21 +38,14 @@ class FilteredSearchVisualTokens {    }    static createVisualTokenElementHTML(canEdit = true) { -    let removeTokenMarkup = ''; -    if (canEdit) { -      removeTokenMarkup = ` -        <div class="remove-token" role="button"> -          <i class="fa fa-close"></i> -        </div> -      `; -    } -      return ` -      <div class="selectable" role="button"> +      <div class="${canEdit ? 'selectable' : 'hidden'}" role="button">          <div class="name"></div>          <div class="value-container">            <div class="value"></div> -          ${removeTokenMarkup} +          <div class="remove-token" role="button"> +            <i class="fa fa-close"></i> +          </div>          </div>        </div>      `; @@ -123,8 +116,8 @@ class FilteredSearchVisualTokens {          /* eslint-disable no-param-reassign */          tokenValueContainer.dataset.originalValue = tokenValue;          tokenValueElement.innerHTML = ` -          <img class="avatar s20" src="${user.avatar_url}" alt="${user.name}'s avatar"> -          ${user.name} +          <img class="avatar s20" src="${user.avatar_url}" alt=""> +          ${_.escape(user.name)}          `;          /* eslint-enable no-param-reassign */        }) diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index ccff8f0ace7..67261c1c9b4 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -1,71 +1,99 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */ - -window.Flash = (function() { -  var hideFlash; - -  hideFlash = function() { -    return $(this).fadeOut(); -  }; - -  /** -   * Flash banner supports different types of Flash configurations -   * along with ability to provide actionConfig which can be used to show -   * additional action or link on banner next to message -   * -   * @param {String} message Flash message -   * @param {String} type Type of Flash, it can be `notice` or `alert` (default) -   * @param {Object} parent Reference to Parent element under which Flash needs to appear -   * @param {Object} actionConfig Map of config to show action on banner -   *    @param {String} href URL to which action link should point (default '#') -   *    @param {String} title Title of action -   *    @param {Function} clickHandler Method to call when action is clicked on -   */ -  function Flash(message, type, parent, actionConfig) { -    var flash, textDiv, actionLink; -    if (type == null) { -      type = 'alert'; -    } -    if (parent == null) { -      parent = null; -    } -    if (parent) { -      this.flashContainer = parent.find('.flash-container'); -    } else { -      this.flashContainer = $('.flash-container-page'); -    } -    this.flashContainer.html(''); -    flash = $('<div/>', { -      "class": "flash-" + type -    }); -    flash.on('click', hideFlash); -    textDiv = $('<div/>', { -      "class": 'flash-text', -      text: message +import _ from 'underscore'; + +const hideFlash = (flashEl, fadeTransition = true) => { +  if (fadeTransition) { +    Object.assign(flashEl.style, { +      transition: 'opacity .3s', +      opacity: '0',      }); -    textDiv.appendTo(flash); +  } -    if (actionConfig) { -      const actionLinkConfig = { -        class: 'flash-action', -        href: actionConfig.href || '#', -        text: actionConfig.title -      }; +  flashEl.addEventListener('transitionend', () => { +    flashEl.remove(); +  }, { +    once: true, +    passive: true, +  }); -      if (!actionConfig.href) { -        actionLinkConfig.role = 'button'; -      } +  if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend')); +}; -      actionLink = $('<a/>', actionLinkConfig); +const createAction = config => ` +  <a +    href="${config.href || '#'}" +    class="flash-action" +    ${config.href ? '' : 'role="button"'} +  > +    ${_.escape(config.title)} +  </a> +`; -      actionLink.appendTo(flash); -      this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler); -    } -    if (this.flashContainer.parent().hasClass('content-wrapper')) { -      textDiv.addClass('container-fluid container-limited'); +const createFlashEl = (message, type, isInContentWrapper = false) => ` +  <div +    class="flash-${type}" +  > +    <div +      class="flash-text ${isInContentWrapper ? 'container-fluid container-limited' : ''}" +    > +      ${_.escape(message)} +    </div> +  </div> +`; + +const removeFlashClickListener = (flashEl, fadeTransition) => { +  flashEl.parentNode.addEventListener('click', () => hideFlash(flashEl, fadeTransition)); +}; + +/* + *  Flash banner supports different types of Flash configurations + *  along with ability to provide actionConfig which can be used to show + *  additional action or link on banner next to message + * + *  @param {String} message           Flash message text + *  @param {String} type              Type of Flash, it can be `notice` or `alert` (default) + *  @param {Object} parent            Reference to parent element under which Flash needs to appear + *  @param {Object} actonConfig       Map of config to show action on banner + *    @param {String} href            URL to which action config should point to (default: '#') + *    @param {String} title           Title of action + *    @param {Function} clickHandler  Method to call when action is clicked on + *  @param {Boolean} fadeTransition   Boolean to determine whether to fade the alert out + */ +const createFlash = function createFlash( +  message, +  type = 'alert', +  parent = document, +  actionConfig = null, +  fadeTransition = true, +) { +  const flashContainer = parent.querySelector('.flash-container'); + +  if (!flashContainer) return null; + +  const isInContentWrapper = flashContainer.parentNode.classList.contains('content-wrapper'); + +  flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper); + +  const flashEl = flashContainer.querySelector(`.flash-${type}`); +  removeFlashClickListener(flashEl, fadeTransition); + +  if (actionConfig) { +    flashEl.innerHTML += createAction(actionConfig); + +    if (actionConfig.clickHandler) { +      flashEl.querySelector('.flash-action').addEventListener('click', e => actionConfig.clickHandler(e));      } -    flash.appendTo(this.flashContainer); -    this.flashContainer.show();    } -  return Flash; -})(); +  flashContainer.style.display = 'block'; + +  return flashContainer; +}; + +export { +  createFlash as default, +  createFlashEl, +  createAction, +  hideFlash, +  removeFlashClickListener, +}; +window.Flash = createFlash; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index e8d8fef8579..c4202f92443 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,6 +1,7 @@  /* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */  /* global fuzzaldrinPlus */  import _ from 'underscore'; +import fuzzaldrinPlus from 'fuzzaldrin-plus';  import { isObject } from './lib/utils/type_utility';  var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput; diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js index 0add7075254..bd63f6f16f0 100644 --- a/app/assets/javascripts/gl_field_error.js +++ b/app/assets/javascripts/gl_field_error.js @@ -54,7 +54,7 @@ const inputErrorClass = 'gl-field-error-outline';  const errorAnchorSelector = '.gl-field-error-anchor';  const ignoreInputSelector = '.gl-field-error-ignore'; -class GlFieldError { +export default class GlFieldError {    constructor({ input, formErrors }) {      this.inputElement = $(input);      this.inputDomElement = this.inputElement.get(0); @@ -159,6 +159,3 @@ class GlFieldError {      this.fieldErrorElement.hide();    }  } - -window.gl = window.gl || {}; -window.gl.GlFieldError = GlFieldError; diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index 4bef60264bb..73bcbd93565 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -1,42 +1,40 @@ -/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */ - -import './gl_field_error'; +import GlFieldError from './gl_field_error';  const customValidationFlag = 'gl-field-error-ignore'; -class GlFieldErrors { +export default class GlFieldErrors {    constructor(form) {      this.form = $(form);      this.state = {        inputs: [], -      valid: false +      valid: false,      };      this.initValidators();    } -  initValidators () { +  initValidators() {      // register selectors here as needed      const validateSelectors = [':text', ':password', '[type=email]'] -      .map((selector) => `input${selector}`).join(','); +      .map(selector => `input${selector}`).join(',');      this.state.inputs = this.form.find(validateSelectors).toArray() -      .filter((input) => !input.classList.contains(customValidationFlag)) -      .map((input) => new window.gl.GlFieldError({ input, formErrors: this })); +      .filter(input => !input.classList.contains(customValidationFlag)) +      .map(input => new GlFieldError({ input, formErrors: this })); -    this.form.on('submit', this.catchInvalidFormSubmit); +    this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit);    }    /* Neccessary to prevent intercept and override invalid form submit     * because Safari & iOS quietly allow form submission when form is invalid     * and prevents disabling of invalid submit button by application.js */ -  catchInvalidFormSubmit (event) { -    const $form = $(event.currentTarget); +  static catchInvalidFormSubmit(e) { +    const $form = $(e.currentTarget);      if (!$form.attr('novalidate')) { -      if (!event.currentTarget.checkValidity()) { -        event.preventDefault(); -        event.stopPropagation(); +      if (!e.currentTarget.checkValidity()) { +        e.preventDefault(); +        e.stopPropagation();        }      }    } @@ -50,11 +48,9 @@ class GlFieldErrors {      });    } -  focusOnFirstInvalid () { -    const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; +  focusOnFirstInvalid() { +    const firstInvalid = this.state.inputs +      .filter(input => !input.inputDomElement.validity.valid)[0];      firstInvalid.inputElement.focus();    }  } - -window.gl = window.gl || {}; -window.gl.GlFieldErrors = GlFieldErrors; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 4e8141b2956..48cd43d3348 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -1,104 +1,99 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */ -/* global GitLab */ -/* global DropzoneInput */  /* global autosize */  import GfmAutoComplete from './gfm_auto_complete'; - -window.gl = window.gl || {}; - -function GLForm(form, enableGFM = false) { -  this.form = form; -  this.textarea = this.form.find('textarea.js-gfm-input'); -  this.enableGFM = enableGFM; -  // Before we start, we should clean up any previous data for this form -  this.destroy(); -  // Setup the form -  this.setupForm(); -  this.form.data('gl-form', this); -} - -GLForm.prototype.destroy = function() { -  // Clean form listeners -  this.clearEventListeners(); -  if (this.autoComplete) { -    this.autoComplete.destroy(); +import dropzoneInput from './dropzone_input'; + +export default class GLForm { +  constructor(form, enableGFM = false) { +    this.form = form; +    this.textarea = this.form.find('textarea.js-gfm-input'); +    this.enableGFM = enableGFM; +    // Before we start, we should clean up any previous data for this form +    this.destroy(); +    // Setup the form +    this.setupForm(); +    this.form.data('gl-form', this);    } -  return this.form.data('gl-form', null); -}; -GLForm.prototype.setupForm = function() { -  var isNewForm; -  isNewForm = this.form.is(':not(.gfm-form)'); -  this.form.removeClass('js-new-note-form'); -  if (isNewForm) { -    this.form.find('.div-dropzone').remove(); -    this.form.addClass('gfm-form'); -    // remove notify commit author checkbox for non-commit notes -    gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); -    this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); -    this.autoComplete.setup(this.form.find('.js-gfm-input'), { -      emojis: true, -      members: this.enableGFM, -      issues: this.enableGFM, -      milestones: this.enableGFM, -      mergeRequests: this.enableGFM, -      labels: this.enableGFM, -    }); -    new DropzoneInput(this.form); -    autosize(this.textarea); +  destroy() { +    // Clean form listeners +    this.clearEventListeners(); +    if (this.autoComplete) { +      this.autoComplete.destroy(); +    } +    this.form.data('gl-form', null);    } -  // form and textarea event listeners -  this.addEventListeners(); -  gl.text.init(this.form); -  // hide discard button -  this.form.find('.js-note-discard').hide(); -  this.form.show(); -  if (this.isAutosizeable) this.setupAutosize(); -}; -GLForm.prototype.setupAutosize = function () { -  this.textarea.off('autosize:resized') -    .on('autosize:resized', this.setHeightData.bind(this)); +  setupForm() { +    const isNewForm = this.form.is(':not(.gfm-form)'); +    this.form.removeClass('js-new-note-form'); +    if (isNewForm) { +      this.form.find('.div-dropzone').remove(); +      this.form.addClass('gfm-form'); +      // remove notify commit author checkbox for non-commit notes +      gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); +      this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); +      this.autoComplete.setup(this.form.find('.js-gfm-input'), { +        emojis: true, +        members: this.enableGFM, +        issues: this.enableGFM, +        milestones: this.enableGFM, +        mergeRequests: this.enableGFM, +        labels: this.enableGFM, +      }); +      dropzoneInput(this.form); +      autosize(this.textarea); +    } +    // form and textarea event listeners +    this.addEventListeners(); +    gl.text.init(this.form); +    // hide discard button +    this.form.find('.js-note-discard').hide(); +    this.form.show(); +    if (this.isAutosizeable) this.setupAutosize(); +  } -  this.textarea.off('mouseup.autosize') -    .on('mouseup.autosize', this.destroyAutosize.bind(this)); +  setupAutosize() { +    this.textarea.off('autosize:resized') +      .on('autosize:resized', this.setHeightData.bind(this)); -  setTimeout(() => { -    autosize(this.textarea); -    this.textarea.css('resize', 'vertical'); -  }, 0); -}; +    this.textarea.off('mouseup.autosize') +      .on('mouseup.autosize', this.destroyAutosize.bind(this)); -GLForm.prototype.setHeightData = function () { -  this.textarea.data('height', this.textarea.outerHeight()); -}; +    setTimeout(() => { +      autosize(this.textarea); +      this.textarea.css('resize', 'vertical'); +    }, 0); +  } -GLForm.prototype.destroyAutosize = function () { -  const outerHeight = this.textarea.outerHeight(); +  setHeightData() { +    this.textarea.data('height', this.textarea.outerHeight()); +  } -  if (this.textarea.data('height') === outerHeight) return; +  destroyAutosize() { +    const outerHeight = this.textarea.outerHeight(); -  autosize.destroy(this.textarea); +    if (this.textarea.data('height') === outerHeight) return; -  this.textarea.data('height', outerHeight); -  this.textarea.outerHeight(outerHeight); -  this.textarea.css('max-height', window.outerHeight); -}; +    autosize.destroy(this.textarea); -GLForm.prototype.clearEventListeners = function() { -  this.textarea.off('focus'); -  this.textarea.off('blur'); -  return gl.text.removeListeners(this.form); -}; +    this.textarea.data('height', outerHeight); +    this.textarea.outerHeight(outerHeight); +    this.textarea.css('max-height', window.outerHeight); +  } -GLForm.prototype.addEventListeners = function() { -  this.textarea.on('focus', function() { -    return $(this).closest('.md-area').addClass('is-focused'); -  }); -  return this.textarea.on('blur', function() { -    return $(this).closest('.md-area').removeClass('is-focused'); -  }); -}; +  clearEventListeners() { +    this.textarea.off('focus'); +    this.textarea.off('blur'); +    gl.text.removeListeners(this.form); +  } -window.gl.GLForm = GLForm; +  addEventListeners() { +    this.textarea.on('focus', function focusTextArea() { +      $(this).closest('.md-area').addClass('is-focused'); +    }); +    this.textarea.on('blur', function blurTextArea() { +      $(this).closest('.md-area').removeClass('is-focused'); +    }); +  } +} diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index cdc4fcf6573..e7232ca3712 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -4,6 +4,7 @@ import _ from 'underscore';  import d3 from 'd3';  import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';  import ContributorsStatGraphUtil from './stat_graph_contributors_util'; +import { n__ } from '../locale';  export default (function() {    function ContributorsStatGraph() {} @@ -44,7 +45,7 @@ export default (function() {      commits = $('<span/>', {        "class": 'graph-author-commits-count'      }); -    commits.text(author.commits + " commits"); +    commits.text(n__('%d commit', '%d commits', author.commits));      return $('<span/>').append(commits);    }; diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index f03b47b1c1d..2168ff3a8ba 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -1,19 +1,12 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */ - -window.GroupAvatar = (function() { -  function GroupAvatar() { -    $('.js-choose-group-avatar-button').on("click", function() { -      var form; -      form = $(this).closest("form"); -      return form.find(".js-group-avatar-input").click(); -    }); -    $('.js-group-avatar-input').on("change", function() { -      var filename, form; -      form = $(this).closest("form"); -      filename = $(this).val().replace(/^.*[\\\/]/, ''); -      return form.find(".js-avatar-filename").text(filename); -    }); -  } - -  return GroupAvatar; -})(); +export default function groupAvatar() { +  $('.js-choose-group-avatar-button').on('click', function onClickGroupAvatar() { +    const form = $(this).closest('form'); +    return form.find('.js-group-avatar-input').click(); +  }); +  $('.js-group-avatar-input').on('change', function onChangeAvatarInput() { +    const form = $(this).closest('form'); +    // eslint-disable-next-line no-useless-escape +    const filename = $(this).val().replace(/^.*[\\\/]/, ''); +    return form.find('.js-avatar-filename').text(filename); +  }); +} diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index 7dc9ce898e8..befaebb635e 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -1,6 +1,4 @@ -/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */ - -class GroupLabelSubscription { +export default class GroupLabelSubscription {    constructor(container) {      const $container = $(container);      this.$dropdown = $container.find('.dropdown'); @@ -18,7 +16,7 @@ class GroupLabelSubscription {      $.ajax({        type: 'POST', -      url: url +      url,      }).done(() => {        this.toggleSubscriptionButtons();        this.$unsubscribeButtons.removeAttr('data-url'); @@ -35,7 +33,7 @@ class GroupLabelSubscription {      $.ajax({        type: 'POST', -      url: url +      url,      }).done(() => {        this.toggleSubscriptionButtons();      }); @@ -47,6 +45,3 @@ class GroupLabelSubscription {      this.$unsubscribeButtons.toggleClass('hidden');    }  } - -window.gl = window.gl || {}; -window.gl.GroupLabelSubscription = GroupLabelSubscription; diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue new file mode 100644 index 00000000000..2c0b6ab4ea8 --- /dev/null +++ b/app/assets/javascripts/groups/components/app.vue @@ -0,0 +1,194 @@ +<script> +/* global Flash */ + +import eventHub from '../event_hub'; +import { getParameterByName } from '../../lib/utils/common_utils'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import { COMMON_STR } from '../constants'; + +import groupsComponent from './groups.vue'; + +export default { +  components: { +    loadingIcon, +    groupsComponent, +  }, +  props: { +    store: { +      type: Object, +      required: true, +    }, +    service: { +      type: Object, +      required: true, +    }, +    hideProjects: { +      type: Boolean, +      required: true, +    }, +  }, +  data() { +    return { +      isLoading: true, +      isSearchEmpty: false, +      searchEmptyMessage: '', +    }; +  }, +  computed: { +    groups() { +      return this.store.getGroups(); +    }, +    pageInfo() { +      return this.store.getPaginationInfo(); +    }, +  }, +  methods: { +    fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) { +      return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived) +                .then((res) => { +                  if (updatePagination) { +                    this.updatePagination(res.headers); +                  } + +                  return res; +                }) +                .then(res => res.json()) +                .catch(() => { +                  this.isLoading = false; +                  $.scrollTo(0); + +                  Flash(COMMON_STR.FAILURE); +                }); +    }, +    fetchAllGroups() { +      const page = getParameterByName('page') || null; +      const sortBy = getParameterByName('sort') || null; +      const archived = getParameterByName('archived') || null; +      const filterGroupsBy = getParameterByName('filter') || null; + +      this.isLoading = true; +      // eslint-disable-next-line promise/catch-or-return +      this.fetchGroups({ +        page, +        filterGroupsBy, +        sortBy, +        archived, +        updatePagination: true, +      }).then((res) => { +        this.isLoading = false; +        this.updateGroups(res, Boolean(filterGroupsBy)); +      }); +    }, +    fetchPage(page, filterGroupsBy, sortBy, archived) { +      this.isLoading = true; + +      // eslint-disable-next-line promise/catch-or-return +      this.fetchGroups({ +        page, +        filterGroupsBy, +        sortBy, +        archived, +        updatePagination: true, +      }).then((res) => { +        this.isLoading = false; +        $.scrollTo(0); + +        const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href); +        window.history.replaceState({ +          page: currentPath, +        }, document.title, currentPath); + +        this.updateGroups(res); +      }); +    }, +    toggleChildren(group) { +      const parentGroup = group; +      if (!parentGroup.isOpen) { +        if (parentGroup.children.length === 0) { +          parentGroup.isChildrenLoading = true; +          // eslint-disable-next-line promise/catch-or-return +          this.fetchGroups({ +            parentId: parentGroup.id, +          }).then((res) => { +            this.store.setGroupChildren(parentGroup, res); +          }).catch(() => { +            parentGroup.isChildrenLoading = false; +          }); +        } else { +          parentGroup.isOpen = true; +        } +      } else { +        parentGroup.isOpen = false; +      } +    }, +    leaveGroup(group, parentGroup) { +      const targetGroup = group; +      targetGroup.isBeingRemoved = true; +      this.service.leaveGroup(targetGroup.leavePath) +        .then(res => res.json()) +        .then((res) => { +          $.scrollTo(0); +          this.store.removeGroup(targetGroup, parentGroup); +          Flash(res.notice, 'notice'); +        }) +        .catch((err) => { +          let message = COMMON_STR.FAILURE; +          if (err.status === 403) { +            message = COMMON_STR.LEAVE_FORBIDDEN; +          } +          Flash(message); +          targetGroup.isBeingRemoved = false; +        }); +    }, +    updatePagination(headers) { +      this.store.setPaginationInfo(headers); +    }, +    updateGroups(groups, fromSearch) { +      this.isSearchEmpty = groups ? groups.length === 0 : false; +      if (fromSearch) { +        this.store.setSearchedGroups(groups); +      } else { +        this.store.setGroups(groups); +      } +    }, +  }, +  created() { +    this.searchEmptyMessage = this.hideProjects ? +      COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY; + +    eventHub.$on('fetchPage', this.fetchPage); +    eventHub.$on('toggleChildren', this.toggleChildren); +    eventHub.$on('leaveGroup', this.leaveGroup); +    eventHub.$on('updatePagination', this.updatePagination); +    eventHub.$on('updateGroups', this.updateGroups); +  }, +  mounted() { +    this.fetchAllGroups(); +  }, +  beforeDestroy() { +    eventHub.$off('fetchPage', this.fetchPage); +    eventHub.$off('toggleChildren', this.toggleChildren); +    eventHub.$off('leaveGroup', this.leaveGroup); +    eventHub.$off('updatePagination', this.updatePagination); +    eventHub.$off('updateGroups', this.updateGroups); +  }, +}; +</script> + +<template> +  <div> +    <loading-icon +      class="loading-animation prepend-top-20" +      size="2" +      v-if="isLoading" +      :label="s__('GroupsTree|Loading groups')" +    /> +    <groups-component +      v-if="!isLoading" +      :groups="groups" +      :search-empty="isSearchEmpty" +      :search-empty-message="searchEmptyMessage" +      :page-info="pageInfo" +    /> +  </div> +</template> diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue index 7cc6c4b0359..e60221fa08d 100644 --- a/app/assets/javascripts/groups/components/group_folder.vue +++ b/app/assets/javascripts/groups/components/group_folder.vue @@ -1,15 +1,27 @@  <script> +import { n__ } from '../../locale'; +import { MAX_CHILDREN_COUNT } from '../constants'; +  export default {    props: { -    groups: { -      type: Object, -      required: true, -    }, -    baseGroup: { +    parentGroup: {        type: Object,        required: false,        default: () => ({}),      }, +    groups: { +      type: Array, +      required: false, +      default: () => ([]), +    }, +  }, +  computed: { +    hasMoreChildren() { +      return this.parentGroup.childrenCount > MAX_CHILDREN_COUNT; +    }, +    moreChildrenStats() { +      return n__('One more item', '%d more items', this.parentGroup.childrenCount - this.parentGroup.children.length); +    },    },  };  </script> @@ -20,8 +32,20 @@ export default {        v-for="(group, index) in groups"        :key="index"        :group="group" -      :base-group="baseGroup" -      :collection="groups" +      :parent-group="parentGroup"      /> +    <li +      v-if="hasMoreChildren" +      class="group-row"> +      <a +        :href="parentGroup.relativePath" +        class="group-row-contents has-more-items"> +        <i +          class="fa fa-external-link" +          aria-hidden="true" +        /> +        {{moreChildrenStats}} +      </a> +    </li>    </ul>  </template> diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 2060410e991..356a95c05ca 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -2,49 +2,28 @@  import identicon from '../../vue_shared/components/identicon.vue';  import eventHub from '../event_hub'; +import itemCaret from './item_caret.vue'; +import itemTypeIcon from './item_type_icon.vue'; +import itemStats from './item_stats.vue'; +import itemActions from './item_actions.vue'; +  export default {    components: {      identicon, +    itemCaret, +    itemTypeIcon, +    itemStats, +    itemActions,    },    props: { -    group: { -      type: Object, -      required: true, -    }, -    baseGroup: { +    parentGroup: {        type: Object,        required: false,        default: () => ({}),      }, -    collection: { +    group: {        type: Object, -      required: false, -      default: () => ({}), -    }, -  }, -  methods: { -    onClickRowGroup(e) { -      e.stopPropagation(); - -      // Skip for buttons -      if (!(e.target.tagName === 'A') && !(e.target.tagName === 'I' && e.target.parentElement.tagName === 'A')) { -        if (this.group.hasSubgroups) { -          eventHub.$emit('toggleSubGroups', this.group); -        } else { -          window.location.href = this.group.groupPath; -        } -      } -    }, -    onLeaveGroup(e) { -      e.preventDefault(); - -      // eslint-disable-next-line no-alert -      if (confirm(`Are you sure you want to leave the "${this.group.fullName}" group?`)) { -        this.leaveGroup(); -      } -    }, -    leaveGroup() { -      eventHub.$emit('leaveGroup', this.group, this.collection); +      required: true,      },    },    computed: { @@ -53,51 +32,33 @@ export default {      },      rowClass() {        return { -        'group-row': true,          'is-open': this.group.isOpen, -        'has-subgroups': this.group.hasSubgroups, -        'no-description': !this.group.description, +        'has-children': this.hasChildren, +        'has-description': this.group.description, +        'being-removed': this.group.isBeingRemoved,        };      }, -    visibilityIcon() { -      return { -        fa: true, -        'fa-globe': this.group.visibility === 'public', -        'fa-shield': this.group.visibility === 'internal', -        'fa-lock': this.group.visibility === 'private', -      }; +    hasChildren() { +      return this.group.childrenCount > 0;      }, -    fullPath() { -      let fullPath = ''; - -      if (this.group.isOrphan) { -        // check if current group is baseGroup -        if (Object.keys(this.baseGroup).length > 0 && this.baseGroup !== this.group) { -          // Remove baseGroup prefix from our current group.fullName. e.g: -          // baseGroup.fullName: `level1` -          // group.fullName: `level1 / level2 / level3` -          // Result: `level2 / level3` -          const gfn = this.group.fullName; -          const bfn = this.baseGroup.fullName; -          const length = bfn.length; -          const start = gfn.indexOf(bfn); -          const extraPrefixChars = 3; - -          fullPath = gfn.substr(start + length + extraPrefixChars); +    hasAvatar() { +      return this.group.avatarUrl !== null; +    }, +    isGroup() { +      return this.group.type === 'group'; +    }, +  }, +  methods: { +    onClickRowGroup(e) { +      const NO_EXPAND_CLS = 'no-expand'; +      if (!(e.target.classList.contains(NO_EXPAND_CLS) || +            e.target.parentElement.classList.contains(NO_EXPAND_CLS))) { +        if (this.hasChildren) { +          eventHub.$emit('toggleChildren', this.group);          } else { -          fullPath = this.group.fullName; +          gl.utils.visitUrl(this.group.relativePath);          } -      } else { -        fullPath = this.group.name;        } - -      return fullPath; -    }, -    hasGroups() { -      return Object.keys(this.group.subGroups).length > 0; -    }, -    hasAvatar() { -      return this.group.avatarUrl && this.group.avatarUrl.indexOf('/assets/no_group_avatar') === -1;      },    },  }; @@ -108,98 +69,36 @@ export default {      @click.stop="onClickRowGroup"      :id="groupDomId"      :class="rowClass" +    class="group-row"      >      <div        class="group-row-contents"> -      <div -        class="controls"> -        <a -          v-if="group.canEdit" -          class="edit-group btn" -          :href="group.editPath"> -          <i -            class="fa fa-cogs" -            aria-hidden="true" -          > -          </i> -        </a> -        <a -          @click="onLeaveGroup" -          :href="group.leavePath" -          class="leave-group btn" -          title="Leave this group"> -          <i -            class="fa fa-sign-out" -            aria-hidden="true" -          > -          </i> -        </a> -      </div> -      <div -        class="stats"> -        <span -          class="number-projects"> -          <i -            class="fa fa-bookmark" -            aria-hidden="true" -          > -          </i> -          {{group.numberProjects}} -        </span> -        <span -          class="number-users"> -          <i -            class="fa fa-users" -            aria-hidden="true" -          > -          </i> -          {{group.numberUsers}} -        </span> -        <span -          class="group-visibility"> -          <i -            :class="visibilityIcon" -            aria-hidden="true" -          > -          </i> -        </span> -      </div> +      <item-actions +        v-if="isGroup" +        :group="group" +        :parent-group="parentGroup" +      /> +      <item-stats +        :item="group" +      />        <div          class="folder-toggle-wrap"> -        <span -          class="folder-caret" -          v-if="group.hasSubgroups"> -          <i -            v-if="group.isOpen" -            class="fa fa-caret-down" -            aria-hidden="true" -          > -          </i> -          <i -            v-if="!group.isOpen" -            class="fa fa-caret-right" -            aria-hidden="true" -          > -          </i> -        </span> -        <span class="folder-icon"> -          <i -            v-if="group.isOpen" -            class="fa fa-folder-open" -            aria-hidden="true" -          > -          </i> -          <i -            v-if="!group.isOpen" -            class="fa fa-folder" -            aria-hidden="true"> -          </i> -        </span> +        <item-caret +          :is-group-open="group.isOpen" +        /> +        <item-type-icon +          :item-type="group.type" +          :is-group-open="group.isOpen" +        />        </div>        <div -        class="avatar-container s40 hidden-xs"> +        class="avatar-container s40 hidden-xs" +        :class="{ 'content-loading': group.isChildrenLoading }" +      >          <a -          :href="group.groupPath"> +          :href="group.relativePath" +          class="no-expand" +        >            <img              v-if="hasAvatar"              class="avatar s40" @@ -215,19 +114,22 @@ export default {        <div          class="title">          <a -          :href="group.groupPath">{{fullPath}}</a> -        <template v-if="group.permissions.humanGroupAccess"> -        as -        <span class="access-type">{{group.permissions.humanGroupAccess}}</span> -        </template> +          :href="group.relativePath" +          class="no-expand">{{group.fullName}}</a> +        <span +          v-if="group.permission" +          class="access-type" +        > +          {{s__('GroupsTreeRole|as')}} {{group.permission}} +        </span>        </div>        <div          class="description">{{group.description}}</div>      </div>      <group-folder -      v-if="group.isOpen && hasGroups" -      :groups="group.subGroups" -      :baseGroup="group" +      v-if="group.isOpen && hasChildren" +      :parent-group="group" +      :groups="group.children"      />    </li>  </template> diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index d17a43b048a..75a2bf34887 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -4,24 +4,33 @@ import eventHub from '../event_hub';  import { getParameterByName } from '../../lib/utils/common_utils';  export default { +  components: { +    tablePagination, +  },    props: {      groups: { -      type: Object, +      type: Array,        required: true,      },      pageInfo: {        type: Object,        required: true,      }, -  }, -  components: { -    tablePagination, +    searchEmpty: { +      type: Boolean, +      required: true, +    }, +    searchEmptyMessage: { +      type: String, +      required: true, +    },    },    methods: {      change(page) {        const filterGroupsParam = getParameterByName('filter_groups');        const sortParam = getParameterByName('sort'); -      eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam); +      const archivedParam = getParameterByName('archived'); +      eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam);      },    },  }; @@ -29,10 +38,17 @@ export default {  <template>    <div class="groups-list-tree-container"> +    <div +      v-if="searchEmpty" +      class="has-no-search-results"> +      {{searchEmptyMessage}} +    </div>      <group-folder +      v-if="!searchEmpty"        :groups="groups"      />      <table-pagination +      v-if="!searchEmpty"        :change="change"        :pageInfo="pageInfo"      /> diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue new file mode 100644 index 00000000000..7eff19e2e5a --- /dev/null +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -0,0 +1,93 @@ +<script> +import { s__ } from '../../locale'; +import tooltip from '../../vue_shared/directives/tooltip'; +import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; +import eventHub from '../event_hub'; +import { COMMON_STR } from '../constants'; + +export default { +  components: { +    PopupDialog, +  }, +  directives: { +    tooltip, +  }, +  props: { +    parentGroup: { +      type: Object, +      required: false, +      default: () => ({}), +    }, +    group: { +      type: Object, +      required: true, +    }, +  }, +  data() { +    return { +      dialogStatus: false, +    }; +  }, +  computed: { +    leaveBtnTitle() { +      return COMMON_STR.LEAVE_BTN_TITLE; +    }, +    editBtnTitle() { +      return COMMON_STR.EDIT_BTN_TITLE; +    }, +    leaveConfirmationMessage() { +      return s__(`GroupsTree|Are you sure you want to leave the "${this.group.fullName}" group?`); +    }, +  }, +  methods: { +    onLeaveGroup() { +      this.dialogStatus = true; +    }, +    leaveGroup(leaveConfirmed) { +      this.dialogStatus = false; +      if (leaveConfirmed) { +        eventHub.$emit('leaveGroup', this.group, this.parentGroup); +      } +    }, +  }, +}; +</script> + +<template> +  <div class="controls"> +    <a +      v-tooltip +      v-if="group.canEdit" +      :href="group.editPath" +      :title="editBtnTitle" +      :aria-label="editBtnTitle" +      data-container="body" +      class="edit-group btn no-expand"> +      <i +        class="fa fa-cogs" +        aria-hidden="true"/> +    </a> +    <a +      v-tooltip +      v-if="group.canLeave" +      @click.prevent="onLeaveGroup" +      :href="group.leavePath" +      :title="leaveBtnTitle" +      :aria-label="leaveBtnTitle" +      data-container="body" +      class="leave-group btn no-expand"> +      <i +        class="fa fa-sign-out" +        aria-hidden="true"/> +    </a> +    <popup-dialog +      v-show="dialogStatus" +      :primary-button-label="__('Leave')" +      kind="warning" +      :title="__('Are you sure?')" +      :text="__('Are you sure you want to leave this group?')" +      :body="leaveConfirmationMessage" +      @submit="leaveGroup" +    /> +  </div> +</template> diff --git a/app/assets/javascripts/groups/components/item_caret.vue b/app/assets/javascripts/groups/components/item_caret.vue new file mode 100644 index 00000000000..959b984816f --- /dev/null +++ b/app/assets/javascripts/groups/components/item_caret.vue @@ -0,0 +1,25 @@ +<script> +export default { +  props: { +    isGroupOpen: { +      type: Boolean, +      required: true, +      default: false, +    }, +  }, +  computed: { +    iconClass() { +      return this.isGroupOpen ? 'fa-caret-down' : 'fa-caret-right'; +    }, +  }, +}; +</script> + +<template> +  <span class="folder-caret"> +    <i +      :class="iconClass" +      class="fa" +      aria-hidden="true"/> +  </span> +</template> diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue new file mode 100644 index 00000000000..9f8ac138fc3 --- /dev/null +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -0,0 +1,98 @@ +<script> +import tooltip from '../../vue_shared/directives/tooltip'; +import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants'; + +export default { +  directives: { +    tooltip, +  }, +  props: { +    item: { +      type: Object, +      required: true, +    }, +  }, +  computed: { +    visibilityIcon() { +      return VISIBILITY_TYPE_ICON[this.item.visibility]; +    }, +    visibilityTooltip() { +      if (this.item.type === ITEM_TYPE.GROUP) { +        return GROUP_VISIBILITY_TYPE[this.item.visibility]; +      } +      return PROJECT_VISIBILITY_TYPE[this.item.visibility]; +    }, +    isProject() { +      return this.item.type === ITEM_TYPE.PROJECT; +    }, +    isGroup() { +      return this.item.type === ITEM_TYPE.GROUP; +    }, +  }, +}; +</script> + +<template> +  <div class="stats"> +    <span +      v-tooltip +      v-if="isGroup" +      :title="s__('Subgroups')" +      class="number-subgroups" +      data-placement="top" +      data-container="body"> +      <i +        class="fa fa-folder" +        aria-hidden="true" +      /> +      {{item.subgroupCount}} +    </span> +    <span +      v-tooltip +      v-if="isGroup" +      :title="s__('Projects')" +      class="number-projects" +      data-placement="top" +      data-container="body"> +      <i +        class="fa fa-bookmark" +        aria-hidden="true" +      /> +      {{item.projectCount}} +    </span> +    <span +      v-tooltip +      v-if="isGroup" +      :title="s__('Members')" +      class="number-users" +      data-placement="top" +      data-container="body"> +      <i +        class="fa fa-users" +        aria-hidden="true" +      /> +      {{item.memberCount}} +    </span> +    <span +      v-if="isProject" +      class="project-stars"> +      <i +        class="fa fa-star" +        aria-hidden="true" +      /> +      {{item.starCount}} +    </span> +    <span +      v-tooltip +      :title="visibilityTooltip" +      data-placement="left" +      data-container="body" +      class="item-visibility"> +      <i +        :class="visibilityIcon" +        class="fa" +        aria-hidden="true" +      /> +    </span> +  </div> +</template> diff --git a/app/assets/javascripts/groups/components/item_type_icon.vue b/app/assets/javascripts/groups/components/item_type_icon.vue new file mode 100644 index 00000000000..c02a8ad6d8c --- /dev/null +++ b/app/assets/javascripts/groups/components/item_type_icon.vue @@ -0,0 +1,34 @@ +<script> +import { ITEM_TYPE } from '../constants'; + +export default { +  props: { +    itemType: { +      type: String, +      required: true, +    }, +    isGroupOpen: { +      type: Boolean, +      required: true, +      default: false, +    }, +  }, +  computed: { +    iconClass() { +      if (this.itemType === ITEM_TYPE.GROUP) { +        return this.isGroupOpen ? 'fa-folder-open' : 'fa-folder'; +      } +      return 'fa-bookmark'; +    }, +  }, +}; +</script> + +<template> +  <span class="item-type-icon"> +    <i +      :class="iconClass" +      class="fa" +      aria-hidden="true"/> +  </span> +</template> diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js new file mode 100644 index 00000000000..6fde41414b3 --- /dev/null +++ b/app/assets/javascripts/groups/constants.js @@ -0,0 +1,35 @@ +import { __, s__ } from '../locale'; + +export const MAX_CHILDREN_COUNT = 20; + +export const COMMON_STR = { +  FAILURE: __('An error occurred. Please try again.'), +  LEAVE_FORBIDDEN: s__('GroupsTree|Failed to leave the group. Please make sure you are not the only owner.'), +  LEAVE_BTN_TITLE: s__('GroupsTree|Leave this group'), +  EDIT_BTN_TITLE: s__('GroupsTree|Edit group'), +  GROUP_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups matched your search'), +  GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups or projects matched your search'), +}; + +export const ITEM_TYPE = { +  PROJECT: 'project', +  GROUP: 'group', +}; + +export const GROUP_VISIBILITY_TYPE = { +  public: __('Public - The group and any public projects can be viewed without any authentication.'), +  internal: __('Internal - The group and any internal projects can be viewed by any logged in user.'), +  private: __('Private - The group and its projects can only be viewed by members.'), +}; + +export const PROJECT_VISIBILITY_TYPE = { +  public: __('Public - The project can be accessed without any authentication.'), +  internal: __('Internal - The project can be accessed by any logged in user.'), +  private: __('Private - Project access must be granted explicitly to each user.'), +}; + +export const VISIBILITY_TYPE_ICON = { +  public: 'fa-globe', +  internal: 'fa-shield', +  private: 'fa-lock', +}; diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 83b102764ba..2db233b09da 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -3,12 +3,13 @@ import eventHub from './event_hub';  import { getParameterByName } from '../lib/utils/common_utils';  export default class GroupFilterableList extends FilterableList { -  constructor({ form, filter, holder, filterEndpoint, pagePath }) { -    super(form, filter, holder); +  constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) { +    super(form, filter, holder, filterInputField);      this.form = form;      this.filterEndpoint = filterEndpoint;      this.pagePath = pagePath; -    this.$dropdown = $('.js-group-filter-dropdown-wrap'); +    this.filterInputField = filterInputField; +    this.$dropdown = $(dropdownSel);    }    getFilterEndpoint() { @@ -24,30 +25,34 @@ export default class GroupFilterableList extends FilterableList {    bindEvents() {      super.bindEvents(); -    this.onFormSubmitWrapper = this.onFormSubmit.bind(this);      this.onFilterOptionClikWrapper = this.onOptionClick.bind(this); -    this.filterForm.addEventListener('submit', this.onFormSubmitWrapper);      this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);    } -  onFormSubmit(e) { -    e.preventDefault(); - -    const $form = $(this.form); -    const filterGroupsParam = $form.find('[name="filter_groups"]').val(); +  onFilterInput() {      const queryData = {}; +    const $form = $(this.form); +    const archivedParam = getParameterByName('archived', window.location.href); +    const filterGroupsParam = $form.find(`[name="${this.filterInputField}"]`).val();      if (filterGroupsParam) { -      queryData.filter_groups = filterGroupsParam; +      queryData[this.filterInputField] = filterGroupsParam; +    } + +    if (archivedParam) { +      queryData.archived = archivedParam;      }      this.filterResults(queryData); -    this.setDefaultFilterOption(); + +    if (this.setDefaultFilterOption) { +      this.setDefaultFilterOption(); +    }    }    setDefaultFilterOption() { -    const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu a:first-child').text()); +    const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').first().text());      this.$dropdown.find('.dropdown-label').text(defaultOption);    } @@ -55,23 +60,42 @@ export default class GroupFilterableList extends FilterableList {      e.preventDefault();      const queryData = {}; -    const sortParam = getParameterByName('sort', e.currentTarget.href); + +    // Get type of option selected from dropdown +    const currentTargetClassList = e.currentTarget.parentElement.classList; +    const isOptionFilterBySort = currentTargetClassList.contains('js-filter-sort-order'); +    const isOptionFilterByArchivedProjects = currentTargetClassList.contains('js-filter-archived-projects'); + +    // Get option query param, also preserve currently applied query param +    const sortParam = getParameterByName('sort', isOptionFilterBySort ? e.currentTarget.href : window.location.href); +    const archivedParam = getParameterByName('archived', isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href);      if (sortParam) {        queryData.sort = sortParam;      } +    if (archivedParam) { +      queryData.archived = archivedParam; +    } +      this.filterResults(queryData);      // Active selected option -    this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text)); +    if (isOptionFilterBySort) { +      this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text)); +      this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').removeClass('is-active'); +    } else if (isOptionFilterByArchivedProjects) { +      this.$dropdown.find('.dropdown-menu li.js-filter-archived-projects a').removeClass('is-active'); +    } + +    $(e.target).addClass('is-active');      // Clear current value on search form -    this.form.querySelector('[name="filter_groups"]').value = ''; +    this.form.querySelector(`[name="${this.filterInputField}"]`).value = '';    }    onFilterSuccess(data, xhr, queryData) { -    super.onFilterSuccess(data, xhr, queryData); +    const currentPath = this.getPagePath(queryData);      const paginationData = {        'X-Per-Page': xhr.getResponseHeader('X-Per-Page'), @@ -82,7 +106,11 @@ export default class GroupFilterableList extends FilterableList {        'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'),      }; -    eventHub.$emit('updateGroups', data); +    window.history.replaceState({ +      page: currentPath, +    }, document.title, currentPath); + +    eventHub.$emit('updateGroups', data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField));      eventHub.$emit('updatePagination', paginationData);    }  } diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 9ad8e5c6052..8b850765a1b 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,17 +1,17 @@ -/* global Flash */ -  import Vue from 'vue'; +import Translate from '../vue_shared/translate';  import GroupFilterableList from './groups_filterable_list'; -import GroupsComponent from './components/groups.vue'; -import GroupFolder from './components/group_folder.vue'; -import GroupItem from './components/group_item.vue'; -import GroupsStore from './stores/groups_store'; -import GroupsService from './services/groups_service'; -import eventHub from './event_hub'; -import { getParameterByName } from '../lib/utils/common_utils'; +import GroupsStore from './store/groups_store'; +import GroupsService from './service/groups_service'; + +import groupsApp from './components/app.vue'; +import groupFolderComponent from './components/group_folder.vue'; +import groupItemComponent from './components/group_item.vue'; + +Vue.use(Translate);  document.addEventListener('DOMContentLoaded', () => { -  const el = document.getElementById('dashboard-group-app'); +  const el = document.getElementById('js-groups-tree');    // Don't do anything if element doesn't exist (No groups)    // This is for when the user enters directly to the page via URL @@ -19,176 +19,56 @@ document.addEventListener('DOMContentLoaded', () => {      return;    } -  Vue.component('groups-component', GroupsComponent); -  Vue.component('group-folder', GroupFolder); -  Vue.component('group-item', GroupItem); +  Vue.component('group-folder', groupFolderComponent); +  Vue.component('group-item', groupItemComponent);    // eslint-disable-next-line no-new    new Vue({      el, +    components: { +      groupsApp, +    },      data() { -      this.store = new GroupsStore(); -      this.service = new GroupsService(el.dataset.endpoint); +      const dataset = this.$options.el.dataset; +      const hideProjects = dataset.hideProjects === 'true'; +      const store = new GroupsStore(hideProjects); +      const service = new GroupsService(dataset.endpoint);        return { -        store: this.store, -        isLoading: true, -        state: this.store.state, +        store, +        service, +        hideProjects,          loading: true,        };      }, -    computed: { -      isEmpty() { -        return Object.keys(this.state.groups).length === 0; -      }, -    }, -    methods: { -      fetchGroups(parentGroup) { -        let parentId = null; -        let getGroups = null; -        let page = null; -        let sort = null; -        let pageParam = null; -        let sortParam = null; -        let filterGroups = null; -        let filterGroupsParam = null; - -        if (parentGroup) { -          parentId = parentGroup.id; -        } else { -          this.isLoading = true; -        } - -        pageParam = getParameterByName('page'); -        if (pageParam) { -          page = pageParam; -        } - -        filterGroupsParam = getParameterByName('filter_groups'); -        if (filterGroupsParam) { -          filterGroups = filterGroupsParam; -        } - -        sortParam = getParameterByName('sort'); -        if (sortParam) { -          sort = sortParam; -        } - -        getGroups = this.service.getGroups(parentId, page, filterGroups, sort); -        getGroups -          .then(response => response.json()) -          .then((response) => { -            this.isLoading = false; - -            this.updateGroups(response, parentGroup); -          }) -          .catch(this.handleErrorResponse); - -        return getGroups; -      }, -      fetchPage(page, filterGroups, sort) { -        this.isLoading = true; - -        return this.service -          .getGroups(null, page, filterGroups, sort) -          .then((response) => { -            this.isLoading = false; -            $.scrollTo(0); - -            const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href); -            window.history.replaceState({ -              page: currentPath, -            }, document.title, currentPath); - -            return response.json().then((data) => { -              this.updateGroups(data); -              this.updatePagination(response.headers); -            }); -          }) -          .catch(this.handleErrorResponse); -      }, -      toggleSubGroups(parentGroup = null) { -        if (!parentGroup.isOpen) { -          this.store.resetGroups(parentGroup); -          this.fetchGroups(parentGroup); -        } - -        this.store.toggleSubGroups(parentGroup); -      }, -      leaveGroup(group, collection) { -        this.service.leaveGroup(group.leavePath) -          .then(resp => resp.json()) -          .then((response) => { -            $.scrollTo(0); - -            this.store.removeGroup(group, collection); - -            // eslint-disable-next-line no-new -            new Flash(response.notice, 'notice'); -          }) -          .catch((error) => { -            let message = 'An error occurred. Please try again.'; - -            if (error.status === 403) { -              message = 'Failed to leave the group. Please make sure you are not the only owner'; -            } - -            // eslint-disable-next-line no-new -            new Flash(message); -          }); -      }, -      updateGroups(groups, parentGroup) { -        this.store.setGroups(groups, parentGroup); -      }, -      updatePagination(headers) { -        this.store.storePagination(headers); -      }, -      handleErrorResponse() { -        this.isLoading = false; -        $.scrollTo(0); - -        // eslint-disable-next-line no-new -        new Flash('An error occurred. Please try again.'); -      }, -    }, -    created() { -      eventHub.$on('fetchPage', this.fetchPage); -      eventHub.$on('toggleSubGroups', this.toggleSubGroups); -      eventHub.$on('leaveGroup', this.leaveGroup); -      eventHub.$on('updateGroups', this.updateGroups); -      eventHub.$on('updatePagination', this.updatePagination); -    },      beforeMount() { +      const dataset = this.$options.el.dataset;        let groupFilterList = null; -      const form = document.querySelector('form#group-filter-form'); -      const filter = document.querySelector('.js-groups-list-filter'); -      const holder = document.querySelector('.js-groups-list-holder'); +      const form = document.querySelector(dataset.formSel); +      const filter = document.querySelector(dataset.filterSel); +      const holder = document.querySelector(dataset.holderSel);        const opts = {          form,          filter,          holder, -        filterEndpoint: el.dataset.endpoint, -        pagePath: el.dataset.path, +        filterEndpoint: dataset.endpoint, +        pagePath: dataset.path, +        dropdownSel: dataset.dropdownSel, +        filterInputField: 'filter',        };        groupFilterList = new GroupFilterableList(opts);        groupFilterList.initSearch();      }, -    mounted() { -      this.fetchGroups() -        .then((response) => { -          this.updatePagination(response.headers); -          this.isLoading = false; -        }) -        .catch(this.handleErrorResponse); -    }, -    beforeDestroy() { -      eventHub.$off('fetchPage', this.fetchPage); -      eventHub.$off('toggleSubGroups', this.toggleSubGroups); -      eventHub.$off('leaveGroup', this.leaveGroup); -      eventHub.$off('updateGroups', this.updateGroups); -      eventHub.$off('updatePagination', this.updatePagination); +    render(createElement) { +      return createElement('groups-app', { +        props: { +          store: this.store, +          service: this.service, +          hideProjects: this.hideProjects, +        }, +      });      },    });  }); diff --git a/app/assets/javascripts/groups/new_group_child.js b/app/assets/javascripts/groups/new_group_child.js new file mode 100644 index 00000000000..8e273579aae --- /dev/null +++ b/app/assets/javascripts/groups/new_group_child.js @@ -0,0 +1,62 @@ +import DropLab from '../droplab/drop_lab'; +import ISetter from '../droplab/plugins/input_setter'; + +const InputSetter = Object.assign({}, ISetter); + +const NEW_PROJECT = 'new-project'; +const NEW_SUBGROUP = 'new-subgroup'; + +export default class NewGroupChild { +  constructor(buttonWrapper) { +    this.buttonWrapper = buttonWrapper; +    this.newGroupChildButton = this.buttonWrapper.querySelector('.js-new-group-child'); +    this.dropdownToggle = this.buttonWrapper.querySelector('.js-dropdown-toggle'); +    this.dropdownList = this.buttonWrapper.querySelector('.dropdown-menu'); + +    this.newGroupPath = this.buttonWrapper.dataset.projectPath; +    this.subgroupPath = this.buttonWrapper.dataset.subgroupPath; + +    this.init(); +  } + +  init() { +    this.initDroplab(); +    this.bindEvents(); +  } + +  initDroplab() { +    this.droplab = new DropLab(); +    this.droplab.init( +      this.dropdownToggle, +      this.dropdownList, +      [InputSetter], +      this.getDroplabConfig(), +    ); +  } + +  getDroplabConfig() { +    return { +      InputSetter: [{ +        input: this.newGroupChildButton, +        valueAttribute: 'data-value', +        inputAttribute: 'data-action', +      }, { +        input: this.newGroupChildButton, +        valueAttribute: 'data-text', +      }], +    }; +  } + +  bindEvents() { +    this.newGroupChildButton +      .addEventListener('click', this.onClickNewGroupChildButton.bind(this)); +  } + +  onClickNewGroupChildButton(e) { +    if (e.target.dataset.action === NEW_PROJECT) { +      gl.utils.visitUrl(this.newGroupPath); +    } else if (e.target.dataset.action === NEW_SUBGROUP) { +      gl.utils.visitUrl(this.subgroupPath); +    } +  } +} diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/service/groups_service.js index 97e02fcb76d..639410384c2 100644 --- a/app/assets/javascripts/groups/services/groups_service.js +++ b/app/assets/javascripts/groups/service/groups_service.js @@ -8,7 +8,7 @@ export default class GroupsService {      this.groups = Vue.resource(endpoint);    } -  getGroups(parentId, page, filterGroups, sort) { +  getGroups(parentId, page, filterGroups, sort, archived) {      const data = {};      if (parentId) { @@ -20,12 +20,16 @@ export default class GroupsService {        }        if (filterGroups) { -        data.filter_groups = filterGroups; +        data.filter = filterGroups;        }        if (sort) {          data.sort = sort;        } + +      if (archived) { +        data.archived = archived; +      }      }      return this.groups.get(data); diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js new file mode 100644 index 00000000000..a1689f4c5cc --- /dev/null +++ b/app/assets/javascripts/groups/store/groups_store.js @@ -0,0 +1,105 @@ +import { normalizeHeaders, parseIntPagination } from '../../lib/utils/common_utils'; + +export default class GroupsStore { +  constructor(hideProjects) { +    this.state = {}; +    this.state.groups = []; +    this.state.pageInfo = {}; +    this.hideProjects = hideProjects; +  } + +  setGroups(rawGroups) { +    if (rawGroups && rawGroups.length) { +      this.state.groups = rawGroups.map(rawGroup => this.formatGroupItem(rawGroup)); +    } else { +      this.state.groups = []; +    } +  } + +  setSearchedGroups(rawGroups) { +    const formatGroups = groups => groups.map((group) => { +      const formattedGroup = this.formatGroupItem(group); +      if (formattedGroup.children && formattedGroup.children.length) { +        formattedGroup.children = formatGroups(formattedGroup.children); +      } +      return formattedGroup; +    }); + +    if (rawGroups && rawGroups.length) { +      this.state.groups = formatGroups(rawGroups); +    } else { +      this.state.groups = []; +    } +  } + +  setGroupChildren(parentGroup, children) { +    const updatedParentGroup = parentGroup; +    updatedParentGroup.children = children.map(rawChild => this.formatGroupItem(rawChild)); +    updatedParentGroup.isOpen = true; +    updatedParentGroup.isChildrenLoading = false; +  } + +  getGroups() { +    return this.state.groups; +  } + +  setPaginationInfo(pagination = {}) { +    let paginationInfo; + +    if (Object.keys(pagination).length) { +      const normalizedHeaders = normalizeHeaders(pagination); +      paginationInfo = parseIntPagination(normalizedHeaders); +    } else { +      paginationInfo = pagination; +    } + +    this.state.pageInfo = paginationInfo; +  } + +  getPaginationInfo() { +    return this.state.pageInfo; +  } + +  formatGroupItem(rawGroupItem) { +    const groupChildren = rawGroupItem.children || []; +    const groupIsOpen = (groupChildren.length > 0) || false; +    const childrenCount = this.hideProjects ? +      rawGroupItem.subgroup_count : +      rawGroupItem.children_count; + +    return { +      id: rawGroupItem.id, +      name: rawGroupItem.name, +      fullName: rawGroupItem.full_name, +      description: rawGroupItem.description, +      visibility: rawGroupItem.visibility, +      avatarUrl: rawGroupItem.avatar_url, +      relativePath: rawGroupItem.relative_path, +      editPath: rawGroupItem.edit_path, +      leavePath: rawGroupItem.leave_path, +      canEdit: rawGroupItem.can_edit, +      canLeave: rawGroupItem.can_leave, +      type: rawGroupItem.type, +      permission: rawGroupItem.permission, +      children: groupChildren, +      isOpen: groupIsOpen, +      isChildrenLoading: false, +      isBeingRemoved: false, +      parentId: rawGroupItem.parent_id, +      childrenCount, +      projectCount: rawGroupItem.project_count, +      subgroupCount: rawGroupItem.subgroup_count, +      memberCount: rawGroupItem.number_users_with_delimiter, +      starCount: rawGroupItem.star_count, +    }; +  } + +  removeGroup(group, parentGroup) { +    const updatedParentGroup = parentGroup; +    if (updatedParentGroup.children && updatedParentGroup.children.length) { +      updatedParentGroup.children = parentGroup.children.filter(child => group.id !== child.id); +    } else { +      this.state.groups = this.state.groups.filter(child => group.id !== child.id); +    } +  } +} diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js deleted file mode 100644 index f59ec677603..00000000000 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ /dev/null @@ -1,167 +0,0 @@ -import Vue from 'vue'; -import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; - -export default class GroupsStore { -  constructor() { -    this.state = {}; -    this.state.groups = {}; -    this.state.pageInfo = {}; -  } - -  setGroups(rawGroups, parent) { -    const parentGroup = parent; -    const tree = this.buildTree(rawGroups, parentGroup); - -    if (parentGroup) { -      parentGroup.subGroups = tree; -    } else { -      this.state.groups = tree; -    } - -    return tree; -  } - -  // eslint-disable-next-line class-methods-use-this -  resetGroups(parent) { -    const parentGroup = parent; -    parentGroup.subGroups = {}; -  } - -  storePagination(pagination = {}) { -    let paginationInfo; - -    if (Object.keys(pagination).length) { -      const normalizedHeaders = normalizeHeaders(pagination); -      paginationInfo = parseIntPagination(normalizedHeaders); -    } else { -      paginationInfo = pagination; -    } - -    this.state.pageInfo = paginationInfo; -  } - -  buildTree(rawGroups, parentGroup) { -    const groups = this.decorateGroups(rawGroups); -    const tree = {}; -    const mappedGroups = {}; -    const orphans = []; - -    // Map groups to an object -    groups.map((group) => { -      mappedGroups[`id${group.id}`] = group; -      mappedGroups[`id${group.id}`].subGroups = {}; -      return group; -    }); - -    Object.keys(mappedGroups).map((key) => { -      const currentGroup = mappedGroups[key]; -      if (currentGroup.parentId) { -        // If the group is not at the root level, add it to its parent array of subGroups. -        const findParentGroup = mappedGroups[`id${currentGroup.parentId}`]; -        if (findParentGroup) { -          mappedGroups[`id${currentGroup.parentId}`].subGroups[`id${currentGroup.id}`] = currentGroup; -          mappedGroups[`id${currentGroup.parentId}`].isOpen = true; // Expand group if it has subgroups -        } else if (parentGroup && parentGroup.id === currentGroup.parentId) { -          tree[`id${currentGroup.id}`] = currentGroup; -        } else { -          // No parent found. We save it for later processing -          orphans.push(currentGroup); - -          // Add to tree to preserve original order -          tree[`id${currentGroup.id}`] = currentGroup; -        } -      } else { -        // If the group is at the top level, add it to first level elements array. -        tree[`id${currentGroup.id}`] = currentGroup; -      } - -      return key; -    }); - -    if (orphans.length) { -      orphans.map((orphan) => { -        let found = false; -        const currentOrphan = orphan; - -        Object.keys(tree).map((key) => { -          const group = tree[key]; - -          if ( -           group && -           currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0 && -           // Make sure the currently selected orphan is not the same as the group -           // we are checking here otherwise it will end up in an infinite loop -           currentOrphan.id !== group.id -           ) { -            group.subGroups[currentOrphan.id] = currentOrphan; -            group.isOpen = true; -            currentOrphan.isOrphan = true; -            found = true; - -            // Delete if group was put at the top level. If not the group will be displayed twice. -            if (tree[`id${currentOrphan.id}`]) { -              delete tree[`id${currentOrphan.id}`]; -            } -          } - -          return key; -        }); - -        if (!found) { -          currentOrphan.isOrphan = true; - -          tree[`id${currentOrphan.id}`] = currentOrphan; -        } - -        return orphan; -      }); -    } - -    return tree; -  } - -  decorateGroups(rawGroups) { -    this.groups = rawGroups.map(this.decorateGroup); -    return this.groups; -  } - -  // eslint-disable-next-line class-methods-use-this -  decorateGroup(rawGroup) { -    return { -      id: rawGroup.id, -      fullName: rawGroup.full_name, -      fullPath: rawGroup.full_path, -      avatarUrl: rawGroup.avatar_url, -      name: rawGroup.name, -      hasSubgroups: rawGroup.has_subgroups, -      canEdit: rawGroup.can_edit, -      description: rawGroup.description, -      webUrl: rawGroup.web_url, -      groupPath: rawGroup.group_path, -      parentId: rawGroup.parent_id, -      visibility: rawGroup.visibility, -      leavePath: rawGroup.leave_path, -      editPath: rawGroup.edit_path, -      isOpen: false, -      isOrphan: false, -      numberProjects: rawGroup.number_projects_with_delimiter, -      numberUsers: rawGroup.number_users_with_delimiter, -      permissions: { -        humanGroupAccess: rawGroup.permissions.human_group_access, -      }, -      subGroups: {}, -    }; -  } - -  // eslint-disable-next-line class-methods-use-this -  removeGroup(group, collection) { -    Vue.delete(collection, `id${group.id}`); -  } - -  // eslint-disable-next-line class-methods-use-this -  toggleSubGroups(toggleGroup) { -    const group = toggleGroup; -    group.isOpen = !group.isOpen; -    return group; -  } -} diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 90ca70289ab..a69a0bde17b 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,121 +1,86 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, -                  camelcase, one-var-declaration-per-line, quotes, object-shorthand, -                  prefer-arrow-callback, comma-dangle, consistent-return, yoda, -                  prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, -                  promise/catch-or-return */  import Api from './api';  import { normalizeCRLFHeaders } from './lib/utils/common_utils'; -var slice = [].slice; +export default function groupsSelect() { +  // Needs to be accessible in rspec +  window.GROUP_SELECT_PER_PAGE = 20; +  $('.ajax-groups-select').each(function setAjaxGroupsSelect2() { +    const $select = $(this); +    const allAvailable = $select.data('all-available'); +    const skipGroups = $select.data('skip-groups') || []; +    $select.select2({ +      placeholder: 'Search for a group', +      multiple: $select.hasClass('multiselect'), +      minimumInputLength: 0, +      ajax: { +        url: Api.buildUrl(Api.groupsPath), +        dataType: 'json', +        quietMillis: 250, +        transport(params) { +          return $.ajax(params) +            .then((data, status, xhr) => { +              const results = data || []; -window.GroupsSelect = (function() { -  function GroupsSelect() { -    $('.ajax-groups-select').each((function(_this) { -      const self = _this; - -      return function(i, select) { -        var all_available, skip_groups; -        const $select = $(select); -        all_available = $select.data('all-available'); -        skip_groups = $select.data('skip-groups') || []; - -        $select.select2({ -          placeholder: "Search for a group", -          multiple: $select.hasClass('multiselect'), -          minimumInputLength: 0, -          ajax: { -            url: Api.buildUrl(Api.groupsPath), -            dataType: 'json', -            quietMillis: 250, -            transport: function (params) { -              $.ajax(params).then((data, status, xhr) => { -                const results = data || []; - -                const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); -                const currentPage = parseInt(headers['X-PAGE'], 10) || 0; -                const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; -                const more = currentPage < totalPages; - -                return { -                  results, -                  pagination: { -                    more, -                  }, -                }; -              }).then(params.success).fail(params.error); -            }, -            data: function (search, page) { -              return { -                search, -                page, -                per_page: GroupsSelect.PER_PAGE, -                all_available, -              }; -            }, -            results: function (data, page) { -              if (data.length) return { results: [] }; - -              const groups = data.length ? data : data.results || []; -              const more = data.pagination ? data.pagination.more : false; -              const results = groups.filter(group => skip_groups.indexOf(group.id) === -1); +              const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); +              const currentPage = parseInt(headers['X-PAGE'], 10) || 0; +              const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; +              const more = currentPage < totalPages;                return {                  results, -                page, -                more, +                pagination: { +                  more, +                },                }; -            }, -          }, -          initSelection: function(element, callback) { -            var id; -            id = $(element).val(); -            if (id !== "") { -              return Api.group(id, callback); -            } -          }, -          formatResult: function() { -            var args; -            args = 1 <= arguments.length ? slice.call(arguments, 0) : []; -            return self.formatResult.apply(self, args); -          }, -          formatSelection: function() { -            var args; -            args = 1 <= arguments.length ? slice.call(arguments, 0) : []; -            return self.formatSelection.apply(self, args); -          }, -          dropdownCssClass: "ajax-groups-dropdown select2-infinite", -          // we do not want to escape markup since we are displaying html in results -          escapeMarkup: function(m) { -            return m; -          } -        }); - -        self.dropdown = document.querySelector('.select2-infinite .select2-results'); - -        $select.on('select2-loaded', self.forceOverflow.bind(self)); -      }; -    })(this)); -  } - -  GroupsSelect.prototype.formatResult = function(group) { -    var avatar; -    if (group.avatar_url) { -      avatar = group.avatar_url; -    } else { -      avatar = gon.default_avatar_url; -    } -    return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>"; -  }; - -  GroupsSelect.prototype.formatSelection = function(group) { -    return group.full_name; -  }; +            }) +            .then(params.success) +            .fail(params.error); +        }, +        data(search, page) { +          return { +            search, +            page, +            per_page: window.GROUP_SELECT_PER_PAGE, +            all_available: allAvailable, +          }; +        }, +        results(data, page) { +          if (data.length) return { results: [] }; -  GroupsSelect.prototype.forceOverflow = function (e) { -    this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`; -  }; +          const groups = data.length ? data : data.results || []; +          const more = data.pagination ? data.pagination.more : false; +          const results = groups.filter(group => skipGroups.indexOf(group.id) === -1); -  GroupsSelect.PER_PAGE = 20; +          return { +            results, +            page, +            more, +          }; +        }, +      }, +      // eslint-disable-next-line consistent-return +      initSelection(element, callback) { +        const id = $(element).val(); +        if (id !== '') { +          return Api.group(id, callback); +        } +      }, +      formatResult(object) { +        return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`; +      }, +      formatSelection(object) { +        return object.full_name; +      }, +      dropdownCssClass: 'ajax-groups-dropdown select2-infinite', +      // we do not want to escape markup since we are displaying html in results +      escapeMarkup(m) { +        return m; +      }, +    }); -  return GroupsSelect; -})(); +    $select.on('select2-loaded', () => { +      const dropdown = document.querySelector('.select2-infinite .select2-results'); +      dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`; +    }); +  }); +} diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index dc170c60456..33a352e158a 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,7 +1,18 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */ +import { highCountTrim } from '~/lib/utils/text_utility'; -$(document).on('todo:toggle', function(e, count) { -  var $todoPendingCount = $('.todos-count'); -  $todoPendingCount.text(gl.text.highCountTrim(count)); -  $todoPendingCount.toggleClass('hidden', count === 0); -}); +/** + * Updates todo counter when todos are toggled. + * When count is 0, we hide the badge. + * + * @param {jQuery.Event} e + * @param {String} count + */ +export default function initTodoToggle() { +  $(document).on('todo:toggle', (e, count) => { +    const parsedCount = parseInt(count, 10); +    const $todoPendingCount = $('.todos-count'); + +    $todoPendingCount.text(highCountTrim(parsedCount)); +    $todoPendingCount.toggleClass('hidden', parsedCount === 0); +  }); +} diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 5b4ca94ed30..1dc70872d92 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -1,83 +1,81 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, vars-on-top, no-new, max-len */ +class ImporterStatus { +  constructor(jobsUrl, importUrl) { +    this.jobsUrl = jobsUrl; +    this.importUrl = importUrl; +    this.initStatusPage(); +    this.setAutoUpdate(); +  } -(function() { -  window.ImporterStatus = (function() { -    function ImporterStatus(jobs_url, import_url) { -      this.jobs_url = jobs_url; -      this.import_url = import_url; -      this.initStatusPage(); -      this.setAutoUpdate(); -    } +  initStatusPage() { +    $('.js-add-to-import') +      .off('click') +      .on('click', (event) => { +        const $btn = $(event.currentTarget); +        const $tr = $btn.closest('tr'); +        const $targetField = $tr.find('.import-target'); +        const $namespaceInput = $targetField.find('.js-select-namespace option:selected'); +        const id = $tr.attr('id').replace('repo_', ''); +        let targetNamespace; +        let newName; +        if ($namespaceInput.length > 0) { +          targetNamespace = $namespaceInput[0].innerHTML; +          newName = $targetField.find('#path').prop('value'); +          $targetField.empty().append(`${targetNamespace}/${newName}`); +        } +        $btn.disable().addClass('is-loading'); -    ImporterStatus.prototype.initStatusPage = function() { -      $('.js-add-to-import').off('click').on('click', (function(_this) { -        return function(e) { -          var $btn, $namespace_input, $target_field, $tr, id, target_namespace, newName; -          $btn = $(e.currentTarget); -          $tr = $btn.closest('tr'); -          $target_field = $tr.find('.import-target'); -          $namespace_input = $target_field.find('.js-select-namespace option:selected'); -          id = $tr.attr('id').replace('repo_', ''); -          target_namespace = null; -          newName = null; -          if ($namespace_input.length > 0) { -            target_namespace = $namespace_input[0].innerHTML; -            newName = $target_field.find('#path').prop('value'); -            $target_field.empty().append(target_namespace + "/" + newName); -          } -          $btn.disable().addClass('is-loading'); -          return $.post(_this.import_url, { -            repo_id: id, -            target_namespace: target_namespace, -            new_name: newName -          }, { -            dataType: 'script' -          }); -        }; -      })(this)); -      return $('.js-import-all').off('click').on('click', function(e) { -        var $btn; -        $btn = $(this); +        return $.post(this.importUrl, { +          repo_id: id, +          target_namespace: targetNamespace, +          new_name: newName, +        }, { +          dataType: 'script', +        }); +      }); + +    $('.js-import-all') +      .off('click') +      .on('click', function onClickImportAll() { +        const $btn = $(this);          $btn.disable().addClass('is-loading'); -        return $('.js-add-to-import').each(function() { +        return $('.js-add-to-import').each(function triggerAddImport() {            return $(this).trigger('click');          });        }); -    }; +  } + +  setAutoUpdate() { +    return setInterval(() => $.get(this.jobsUrl, data => $.each(data, (i, job) => { +      const jobItem = $(`#project_${job.id}`); +      const statusField = jobItem.find('.job-status'); -    ImporterStatus.prototype.setAutoUpdate = function() { -      return setInterval(((function(_this) { -        return function() { -          return $.get(_this.jobs_url, function(data) { -            return $.each(data, function(i, job) { -              var job_item, status_field; -              job_item = $("#project_" + job.id); -              status_field = job_item.find(".job-status"); -              if (job.import_status === 'finished') { -                job_item.removeClass("active").addClass("success"); -                return status_field.html('<span><i class="fa fa-check"></i> done</span>'); -              } else if (job.import_status === 'scheduled') { -                return status_field.html("<i class='fa fa-spinner fa-spin'></i> scheduled"); -              } else if (job.import_status === 'started') { -                return status_field.html("<i class='fa fa-spinner fa-spin'></i> started"); -              } else { -                return status_field.html(job.import_status); -              } -            }); -          }); -        }; -      })(this)), 4000); -    }; +      const spinner = '<i class="fa fa-spinner fa-spin"></i>'; -    return ImporterStatus; -  })(); +      switch (job.import_status) { +        case 'finished': +          jobItem.removeClass('active').addClass('success'); +          statusField.html('<span><i class="fa fa-check"></i> done</span>'); +          break; +        case 'scheduled': +          statusField.html(`${spinner} scheduled`); +          break; +        case 'started': +          statusField.html(`${spinner} started`); +          break; +        default: +          statusField.html(job.import_status); +          break; +      } +    })), 4000); +  } +} -  $(function() { -    if ($('.js-importer-status').length) { -      var jobsImportPath = $('.js-importer-status').data('jobs-import-path'); -      var importPath = $('.js-importer-status').data('import-path'); +// eslint-disable-next-line consistent-return +export default function initImporterStatus() { +  const importerStatus = document.querySelector('.js-importer-status'); -      new window.ImporterStatus(jobsImportPath, importPath); -    } -  }); -}).call(window); +  if (importerStatus) { +    const data = importerStatus.dataset; +    return new ImporterStatus(data.jobsImportPath, data.importPath); +  } +} diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js index f785ed29e6c..1bab7965c19 100644 --- a/app/assets/javascripts/init_changes_dropdown.js +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -1,7 +1,7 @@  import stickyMonitor from './lib/utils/sticky'; -export default () => { -  stickyMonitor(document.querySelector('.js-diff-files-changed')); +export default (stickyTop) => { +  stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);    $('.js-diff-stats-dropdown').glDropdown({      filterable: true, diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js index 29e3d2ea94e..1191e0b895e 100644 --- a/app/assets/javascripts/init_issuable_sidebar.js +++ b/app/assets/javascripts/init_issuable_sidebar.js @@ -1,9 +1,11 @@  /* eslint-disable no-new */  /* global MilestoneSelect */ -/* global LabelsSelect */ -/* global IssuableContext */ +import LabelsSelect from './labels_select'; +import IssuableContext from './issuable_context';  /* global Sidebar */ +import DueDateSelectors from './due_date_select'; +  export default () => {    const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); @@ -13,6 +15,6 @@ export default () => {    new LabelsSelect();    new IssuableContext(sidebarOptions.currentUser);    gl.Subscription.bindAll('.subscription'); -  new gl.DueDateSelectors(); +  new DueDateSelectors();    window.sidebar = new Sidebar();  }; diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js index 1211c2c802c..1b265721581 100644 --- a/app/assets/javascripts/init_legacy_filters.js +++ b/app/assets/javascripts/init_legacy_filters.js @@ -1,15 +1,15 @@  /* eslint-disable no-new */ -/* global LabelsSelect */ +import LabelsSelect from './labels_select';  /* global MilestoneSelect */ -/* global IssueStatusSelect */  /* global SubscriptionSelect */  import UsersSelect from './users_select'; +import issueStatusSelect from './issue_status_select';  export default () => {    new UsersSelect();    new LabelsSelect();    new MilestoneSelect(); -  new IssueStatusSelect(); +  issueStatusSelect();    new SubscriptionSelect();  }; diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index cf1e6a14725..32415a8791f 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -1,4 +1,4 @@ -/* global Flash */ +import Flash from '../flash';  export default class IntegrationSettingsForm {    constructor(formSelector) { @@ -102,7 +102,7 @@ export default class IntegrationSettingsForm {      })      .done((res) => {        if (res.error) { -        new Flash(`${res.message} ${res.service_response}`, null, null, { +        new Flash(`${res.message} ${res.service_response}`, 'alert', document, {            title: 'Save anyway',            clickHandler: (e) => {              e.preventDefault(); diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index c39ffdb2e0f..b124fafec70 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -1,7 +1,6 @@  /* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ -/* global IssuableIndex */ -/* global Flash */  import _ from 'underscore'; +import Flash from './flash';  export default {    init({ container, form, issues, prefixId } = {}) { diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index 0e8a0519928..af6358953cf 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -1,10 +1,12 @@  /* eslint-disable class-methods-use-this, no-new */ -/* global LabelsSelect */  /* global MilestoneSelect */ -/* global IssueStatusSelect */  /* global SubscriptionSelect */  import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; +import './milestone_select'; +import issueStatusSelect from './issue_status_select'; +import './subscription_select'; +import LabelsSelect from './labels_select';  const HIDDEN_CLASS = 'hidden';  const DISABLED_CONTENT_CLASS = 'disabled-content'; @@ -45,7 +47,7 @@ export default class IssuableBulkUpdateSidebar {    initDropdowns() {      new LabelsSelect();      new MilestoneSelect(); -    new IssueStatusSelect(); +    issueStatusSelect();      new SubscriptionSelect();    } diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 1d305f1eb2f..da99394ff90 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,33 +1,32 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */  import Cookies from 'js-cookie';  import bp from './breakpoints';  import UsersSelect from './users_select'; -const PARTICIPANTS_ROW_COUNT = 7; +export default class IssuableContext { +  constructor(currentUser) { +    this.userSelect = new UsersSelect(currentUser); -(function() { -  this.IssuableContext = (function() { -    function IssuableContext(currentUser) { -      this.initParticipants(); -      new UsersSelect(currentUser); -      $('select.select2').select2({ -        width: 'resolve', -        dropdownAutoWidth: true -      }); -      $(".issuable-sidebar .inline-update").on("change", "select", function() { -        return $(this).submit(); -      }); -      $(".issuable-sidebar .inline-update").on("change", ".js-assignee", function() { -        return $(this).submit(); -      }); -      $(document).off('click', '.issuable-sidebar .dropdown-content a').on('click', '.issuable-sidebar .dropdown-content a', function(e) { -        return e.preventDefault(); -      }); -      $(document).off('click', '.edit-link').on('click', '.edit-link', function(e) { -        var $block, $selectbox; +    $('select.select2').select2({ +      width: 'resolve', +      dropdownAutoWidth: true, +    }); + +    $('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() { +      return $(this).submit(); +    }); +    $('.issuable-sidebar .inline-update').on('change', '.js-assignee', function onClickAssignee() { +      return $(this).submit(); +    }); +    $(document) +      .off('click', '.issuable-sidebar .dropdown-content a') +      .on('click', '.issuable-sidebar .dropdown-content a', e => e.preventDefault()); + +    $(document) +      .off('click', '.edit-link') +      .on('click', '.edit-link', function onClickEdit(e) {          e.preventDefault(); -        $block = $(this).parents('.block'); -        $selectbox = $block.find('.selectbox'); +        const $block = $(this).parents('.block'); +        const $selectbox = $block.find('.selectbox');          if ($selectbox.is(':visible')) {            $selectbox.hide();            $block.find('.value').show(); @@ -35,47 +34,18 @@ const PARTICIPANTS_ROW_COUNT = 7;            $selectbox.show();            $block.find('.value').hide();          } -        if ($selectbox.is(':visible')) { -          return setTimeout(function() { -            return $block.find('.dropdown-menu-toggle').trigger('click'); -          }, 0); -        } -      }); -      window.addEventListener('beforeunload', function() { -        // collapsed_gutter cookie hides the sidebar -        var bpBreakpoint = bp.getBreakpointSize(); -        if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') { -          Cookies.set('collapsed_gutter', true); -        } -      }); -    } -    IssuableContext.prototype.initParticipants = function() { -      $(document).on("click", ".js-participants-more", this.toggleHiddenParticipants); -      return $(".js-participants-author").each(function(i) { -        if (i >= PARTICIPANTS_ROW_COUNT) { -          return $(this).addClass("js-participants-hidden").hide(); +        if ($selectbox.is(':visible')) { +          setTimeout(() => $block.find('.dropdown-menu-toggle').trigger('click'), 0);          }        }); -    }; -    IssuableContext.prototype.toggleHiddenParticipants = function(e) { -      var currentText, lessText, originalText; -      e.preventDefault(); -      currentText = $(this).text().trim(); -      lessText = $(this).data("less-text"); -      originalText = $(this).data("original-text"); -      if (currentText === originalText) { -        $(this).text(lessText); - -        if (gl.lazyLoader) gl.lazyLoader.loadCheck(); -      } else { -        $(this).text(originalText); +    window.addEventListener('beforeunload', () => { +      // collapsed_gutter cookie hides the sidebar +      const bpBreakpoint = bp.getBreakpointSize(); +      if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') { +        Cookies.set('collapsed_gutter', true);        } - -      $(".js-participants-hidden").toggle(); -    }; - -    return IssuableContext; -  })(); -}).call(window); +    }); +  } +} diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 470c39c6f76..57dcaa0e1ac 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,108 +1,107 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ +/* eslint-disable func-names, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */  /* global GitLab */ -/* global Autosave */ -/* global dateFormat */  import Pikaday from 'pikaday'; +import Autosave from './autosave';  import UsersSelect from './users_select';  import GfmAutoComplete from './gfm_auto_complete';  import ZenMode from './zen_mode'; +import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; -(function() { -  this.IssuableForm = (function() { -    IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i; - -    function IssuableForm(form) { -      var $issuableDueDate, calendar; -      this.form = form; -      this.toggleWip = this.toggleWip.bind(this); -      this.renderWipExplanation = this.renderWipExplanation.bind(this); -      this.resetAutosave = this.resetAutosave.bind(this); -      this.handleSubmit = this.handleSubmit.bind(this); -      new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); -      new UsersSelect(); -      new ZenMode(); -      this.titleField = this.form.find("input[name*='[title]']"); -      this.descriptionField = this.form.find("textarea[name*='[description]']"); -      if (!(this.titleField.length && this.descriptionField.length)) { -        return; -      } -      this.initAutosave(); -      this.form.on("submit", this.handleSubmit); -      this.form.on("click", ".btn-cancel", this.resetAutosave); -      this.initWip(); -      $issuableDueDate = $('#issuable-due-date'); -      if ($issuableDueDate.length) { -        calendar = new Pikaday({ -          field: $issuableDueDate.get(0), -          theme: 'gitlab-theme animate-picker', -          format: 'yyyy-mm-dd', -          container: $issuableDueDate.parent().get(0), -          onSelect: function(dateText) { -            $issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); -          } -        }); -        calendar.setDate(new Date($issuableDueDate.val())); -      } +export default class IssuableForm { +  constructor(form) { +    this.form = form; +    this.toggleWip = this.toggleWip.bind(this); +    this.renderWipExplanation = this.renderWipExplanation.bind(this); +    this.resetAutosave = this.resetAutosave.bind(this); +    this.handleSubmit = this.handleSubmit.bind(this); +    this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i; + +    new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); +    new UsersSelect(); +    new ZenMode(); + +    this.titleField = this.form.find('input[name*="[title]"]'); +    this.descriptionField = this.form.find('textarea[name*="[description]"]'); +    if (!(this.titleField.length && this.descriptionField.length)) { +      return;      } -    IssuableForm.prototype.initAutosave = function() { -      new Autosave(this.titleField, [document.location.pathname, document.location.search, "title"]); -      return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, "description"]); -    }; - -    IssuableForm.prototype.handleSubmit = function() { -      return this.resetAutosave(); -    }; - -    IssuableForm.prototype.resetAutosave = function() { -      this.titleField.data("autosave").reset(); -      return this.descriptionField.data("autosave").reset(); -    }; - -    IssuableForm.prototype.initWip = function() { -      this.$wipExplanation = this.form.find(".js-wip-explanation"); -      this.$noWipExplanation = this.form.find(".js-no-wip-explanation"); -      if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) { -        return; -      } -      this.form.on("click", ".js-toggle-wip", this.toggleWip); -      this.titleField.on("keyup blur", this.renderWipExplanation); -      return this.renderWipExplanation(); -    }; - -    IssuableForm.prototype.workInProgress = function() { -      return this.wipRegex.test(this.titleField.val()); -    }; - -    IssuableForm.prototype.renderWipExplanation = function() { -      if (this.workInProgress()) { -        this.$wipExplanation.show(); -        return this.$noWipExplanation.hide(); -      } else { -        this.$wipExplanation.hide(); -        return this.$noWipExplanation.show(); -      } -    }; - -    IssuableForm.prototype.toggleWip = function(event) { -      event.preventDefault(); -      if (this.workInProgress()) { -        this.removeWip(); -      } else { -        this.addWip(); -      } -      return this.renderWipExplanation(); -    }; - -    IssuableForm.prototype.removeWip = function() { -      return this.titleField.val(this.titleField.val().replace(this.wipRegex, "")); -    }; - -    IssuableForm.prototype.addWip = function() { -      return this.titleField.val("WIP: " + (this.titleField.val())); -    }; - -    return IssuableForm; -  })(); -}).call(window); +    this.initAutosave(); +    this.form.on('submit', this.handleSubmit); +    this.form.on('click', '.btn-cancel', this.resetAutosave); +    this.initWip(); + +    const $issuableDueDate = $('#issuable-due-date'); + +    if ($issuableDueDate.length) { +      const calendar = new Pikaday({ +        field: $issuableDueDate.get(0), +        theme: 'gitlab-theme animate-picker', +        format: 'yyyy-mm-dd', +        container: $issuableDueDate.parent().get(0), +        parse: dateString => parsePikadayDate(dateString), +        toString: date => pikadayToString(date), +        onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)), +      }); +      calendar.setDate(parsePikadayDate($issuableDueDate.val())); +    } +  } + +  initAutosave() { +    new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']); +    return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']); +  } + +  handleSubmit() { +    return this.resetAutosave(); +  } + +  resetAutosave() { +    this.titleField.data('autosave').reset(); +    return this.descriptionField.data('autosave').reset(); +  } + +  initWip() { +    this.$wipExplanation = this.form.find('.js-wip-explanation'); +    this.$noWipExplanation = this.form.find('.js-no-wip-explanation'); +    if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) { +      return; +    } +    this.form.on('click', '.js-toggle-wip', this.toggleWip); +    this.titleField.on('keyup blur', this.renderWipExplanation); +    return this.renderWipExplanation(); +  } + +  workInProgress() { +    return this.wipRegex.test(this.titleField.val()); +  } + +  renderWipExplanation() { +    if (this.workInProgress()) { +      this.$wipExplanation.show(); +      return this.$noWipExplanation.hide(); +    } else { +      this.$wipExplanation.hide(); +      return this.$noWipExplanation.show(); +    } +  } + +  toggleWip(event) { +    event.preventDefault(); +    if (this.workInProgress()) { +      this.removeWip(); +    } else { +      this.addWip(); +    } +    return this.renderWipExplanation(); +  } + +  removeWip() { +    return this.titleField.val(this.titleField.val().replace(this.wipRegex, '')); +  } + +  addWip() { +    this.titleField.val(`WIP: ${(this.titleField.val())}`); +  } +} diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index ece0220c927..0b123a11a3b 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -1,171 +1,42 @@ -/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */ -/* global IssuableIndex */ -import _ from 'underscore';  import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';  import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; -((global) => { -  var issuable_created; - -  issuable_created = false; - -  global.IssuableIndex = { -    init: function(pagePrefix) { -      IssuableIndex.initTemplates(); -      IssuableIndex.initSearch(); -      IssuableIndex.initBulkUpdate(pagePrefix); -      IssuableIndex.initResetFilters(); -      IssuableIndex.resetIncomingEmailToken(); -      IssuableIndex.initLabelFilterRemove(); -    }, -    initTemplates: function() { -      return IssuableIndex.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>'); -    }, -    initSearch: function() { -      const $searchInput = $('#issuable_search'); - -      IssuableIndex.initSearchState($searchInput); - -      // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing -      const debouncedExecSearch = _.debounce(IssuableIndex.executeSearch, 1000, false); - -      $searchInput.off('keyup').on('keyup', debouncedExecSearch); - -      // ensures existing filters are preserved when manually submitted -      $('#issuable_search_form').on('submit', (e) => { -        e.preventDefault(); -        debouncedExecSearch(e); -      }); -    }, -    initSearchState: function($searchInput) { -      const currentSearchVal = $searchInput.val(); - -      IssuableIndex.searchState = { -        elem: $searchInput, -        current: currentSearchVal -      }; - -      IssuableIndex.maybeFocusOnSearch(); -    }, -    accessSearchPristine: function(set) { -      // store reference to previous value to prevent search on non-mutating keyup -      const state = IssuableIndex.searchState; -      const currentSearchVal = state.elem.val(); - -      if (set) { -        state.current = currentSearchVal; -      } else { -        return state.current === currentSearchVal; -      } -    }, -    maybeFocusOnSearch: function() { -      const currentSearchVal = IssuableIndex.searchState.current; -      if (currentSearchVal && currentSearchVal !== '') { -        const queryLength = currentSearchVal.length; -        const $searchInput = IssuableIndex.searchState.elem; - -      /* The following ensures that the cursor is initially placed at -        * the end of search input when focus is applied. It accounts -        * for differences in browser implementations of `setSelectionRange` -        * and cursor placement for elements in focus. -      */ -        $searchInput.focus(); -        if ($searchInput.setSelectionRange) { -          $searchInput.setSelectionRange(queryLength, queryLength); -        } else { -          $searchInput.val(currentSearchVal); -        } -      } -    }, -    executeSearch: function(e) { -      const $search = $('#issuable_search'); -      const $searchName = $search.attr('name'); -      const $searchValue = $search.val(); -      const $filtersForm = $('.js-filter-form'); -      const $input = $(`input[name='${$searchName}']`, $filtersForm); -      const isPristine = IssuableIndex.accessSearchPristine(); - -      if (isPristine) { -        return; -      } - -      if (!$input.length) { -        $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`); -      } else { -        $input.val($searchValue); -      } - -      IssuableIndex.filterResults($filtersForm); -    }, -    initLabelFilterRemove: function() { -      return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { -        var $button; -        $button = $(this); -        // Remove the label input box -        $('input[name="label_name[]"]').filter(function() { -          return this.value === $button.data('label'); -        }).remove(); -        // Submit the form to get new data -        IssuableIndex.filterResults($('.filter-form')); -      }); -    }, -    filterResults: (function(_this) { -      return function(form) { -        var formAction, formData, issuesUrl; -        formData = form.serializeArray(); -        formData = formData.filter(function(data) { -          return data.value !== ''; -        }); -        formData = $.param(formData); -        formAction = form.attr('action'); -        issuesUrl = formAction; -        issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&'); -        issuesUrl += formData; -        return gl.utils.visitUrl(issuesUrl); -      }; -    })(this), -    initResetFilters: function() { -      $('.reset-filters').on('click', function(e) { -        e.preventDefault(); -        const target = e.target; -        const $form = $(target).parents('.js-filter-form'); -        const baseIssuesUrl = target.href; - -        $form.attr('action', baseIssuesUrl); -        gl.utils.visitUrl(baseIssuesUrl); +export default class IssuableIndex { +  constructor(pagePrefix) { +    this.initBulkUpdate(pagePrefix); +    IssuableIndex.resetIncomingEmailToken(); +  } +  initBulkUpdate(pagePrefix) { +    const userCanBulkUpdate = $('.issues-bulk-update').length > 0; +    const alreadyInitialized = !!this.bulkUpdateSidebar; + +    if (userCanBulkUpdate && !alreadyInitialized) { +      IssuableBulkUpdateActions.init({ +        prefixId: pagePrefix,        }); -    }, -    initBulkUpdate: function(pagePrefix) { -      const userCanBulkUpdate = $('.issues-bulk-update').length > 0; -      const alreadyInitialized = !!this.bulkUpdateSidebar; - -      if (userCanBulkUpdate && !alreadyInitialized) { -        IssuableBulkUpdateActions.init({ -          prefixId: pagePrefix, -        }); - -        this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar(); -      } -    }, -    resetIncomingEmailToken: function() { -      $('.incoming-email-token-reset').on('click', function(e) { -        e.preventDefault(); -        $.ajax({ -          type: 'PUT', -          url: $('.incoming-email-token-reset').attr('href'), -          dataType: 'json', -          success: function(response) { -            $('#issue_email').val(response.new_issue_address).focus(); -          }, -          beforeSend: function() { -            $('.incoming-email-token-reset').text('resetting...'); -          }, -          complete: function() { -            $('.incoming-email-token-reset').text('reset it'); -          } -        }); -      }); +      this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();      } -  }; -})(window); +  } + +  static resetIncomingEmailToken() { +    $('.incoming-email-token-reset').on('click', (e) => { +      e.preventDefault(); + +      $.ajax({ +        type: 'PUT', +        url: $('.incoming-email-token-reset').attr('href'), +        dataType: 'json', +        success(response) { +          $('#issue_email').val(response.new_issue_address).focus(); +        }, +        beforeSend() { +          $('.incoming-email-token-reset').text('resetting...'); +        }, +        complete() { +          $('.incoming-email-token-reset').text('reset it'); +        }, +      }); +    }); +  } +} diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index c0bd64814ca..acd5730cf3c 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,14 +1,12 @@  /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */ -/* global Flash */ -  import 'vendor/jquery.waitforimages';  import '~/lib/utils/text_utility'; -import './flash'; +import Flash from './flash';  import TaskList from './task_list';  import CreateMergeRequestDropdown from './create_merge_request_dropdown';  import IssuablesHelper from './helpers/issuables_helper'; -class Issue { +export default class Issue {    constructor() {      if ($('a.btn-close').length) {        this.taskList = new TaskList({ @@ -149,5 +147,3 @@ class Issue {      });    }  } - -export default Issue; diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 06f6ec241f4..d1aa83ea57f 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -1,5 +1,4 @@  <script> -/* global Flash */  import Visibility from 'visibilityjs';  import Poll from '../../lib/utils/poll';  import eventHub from '../event_hub'; @@ -25,6 +24,11 @@ export default {        required: true,        type: Boolean,      }, +    showInlineEditButton: { +      type: Boolean, +      required: false, +      default: false, +    },      issuableRef: {        type: String,        required: true, @@ -153,7 +157,7 @@ export default {          })          .catch(() => {            eventHub.$emit('close.form'); -          return new Flash('Error updating issue'); +          window.Flash('Error updating issue');          });      },      deleteIssuable() { @@ -167,7 +171,7 @@ export default {          })          .catch(() => {            eventHub.$emit('close.form'); -          return new Flash('Error deleting issue'); +          window.Flash('Error deleting issue');          });      },    }, @@ -223,20 +227,25 @@ export default {      <div v-else>        <title-component          :issuable-ref="issuableRef" +        :can-update="canUpdate"          :title-html="state.titleHtml" -        :title-text="state.titleText" /> +        :title-text="state.titleText" +        :show-inline-edit-button="showInlineEditButton" +      />        <description-component          v-if="state.descriptionHtml"          :can-update="canUpdate"          :description-html="state.descriptionHtml"          :description-text="state.descriptionText"          :updated-at="state.updatedAt" -        :task-status="state.taskStatus" /> +        :task-status="state.taskStatus" +      />        <edited-component          v-if="hasUpdated"          :updated-at="state.updatedAt"          :updated-by-name="state.updatedByName" -        :updated-by-path="state.updatedByPath" /> +        :updated-by-path="state.updatedByPath" +      />      </div>    </div>  </template> diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index dc902eefc5f..0aa1b2c2e31 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -1,5 +1,4 @@  <script> -  /* global Flash */    import updateMixin from '../../mixins/update';    import markdownField from '../../../vue_shared/components/markdown/field.vue'; diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue index 83af8e1e245..c3abb9fd9d5 100644 --- a/app/assets/javascripts/issue_show/components/fields/title.vue +++ b/app/assets/javascripts/issue_show/components/fields/title.vue @@ -16,15 +16,15 @@    <fieldset>      <label        class="sr-only" -      for="issue-title"> +      for="issuable-title">        Title      </label>      <input -      id="issue-title" +      id="issuable-title"        class="form-control"        type="text" -      placeholder="Issue title" -      aria-label="Issue title" +      placeholder="Title" +      aria-label="Title"        v-model="formState.title"        @keydown.meta.enter="updateIssuable"        @keydown.ctrl.enter="updateIssuable" /> diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index a9dabd4cff1..00002709ac6 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -1,5 +1,8 @@  <script>    import animateMixin from '../mixins/animate'; +  import eventHub from '../event_hub'; +  import tooltip from '../../vue_shared/directives/tooltip'; +  import { spriteIcon } from '../../lib/utils/common_utils';    export default {      mixins: [animateMixin], @@ -15,6 +18,11 @@          type: String,          required: true,        }, +      canUpdate: { +        required: false, +        type: Boolean, +        default: false, +      },        titleHtml: {          type: String,          required: true, @@ -23,6 +31,14 @@          type: String,          required: true,        }, +      showInlineEditButton: { +        type: Boolean, +        required: false, +        default: false, +      }, +    }, +    directives: { +      tooltip,      },      watch: {        titleHtml() { @@ -30,24 +46,46 @@          this.animateChange();        },      }, +    computed: { +      pencilIcon() { +        return spriteIcon('pencil', 'link-highlight'); +      }, +    },      methods: {        setPageTitle() {          const currentPageTitleScope = this.titleEl.innerText.split('·');          currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;          this.titleEl.textContent = currentPageTitleScope.join('·');        }, +      edit() { +        eventHub.$emit('open.form'); +      },      },    };  </script>  <template> -  <h2 -    class="title" -    :class="{ -      'issue-realtime-pre-pulse': preAnimation, -      'issue-realtime-trigger-pulse': pulseAnimation -    }" -    v-html="titleHtml" -  > -  </h2> +  <div class="title-container"> +    <h2 +      class="title" +      :class="{ +        'issue-realtime-pre-pulse': preAnimation, +        'issue-realtime-trigger-pulse': pulseAnimation +      }" +      v-html="titleHtml" +    > +    </h2> +    <button +      v-tooltip +      v-if="showInlineEditButton && canUpdate" +      type="button" +      class="btn-blank btn-edit note-action-button" +      v-html="pencilIcon" +      title="Edit title and description" +      data-placement="bottom" +      data-container="body" +      @click="edit" +      > +    </button> +  </div>  </template> diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js index 56cb536dcde..03546f61d1f 100644 --- a/app/assets/javascripts/issue_status_select.js +++ b/app/assets/javascripts/issue_status_select.js @@ -1,34 +1,23 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */ -(function() { -  this.IssueStatusSelect = (function() { -    function IssueStatusSelect() { -      $('.js-issue-status').each(function(i, el) { -        var fieldName; -        fieldName = $(el).data("field-name"); -        return $(el).glDropdown({ -          selectable: true, -          fieldName: fieldName, -          toggleLabel: (function(_this) { -            return function(selected, el, instance) { -              var $item, label; -              label = 'Author'; -              $item = instance.dropdown.find('.is-active'); -              if ($item.length) { -                label = $item.text(); -              } -              return label; -            }; -          })(this), -          clicked: function(options) { -            return options.e.preventDefault(); -          }, -          id: function(obj, el) { -            return $(el).data("id"); -          } -        }); -      }); -    } - -    return IssueStatusSelect; -  })(); -}).call(window); +export default function issueStatusSelect() { +  $('.js-issue-status').each((i, el) => { +    const fieldName = $(el).data('field-name'); +    return $(el).glDropdown({ +      selectable: true, +      fieldName, +      toggleLabel(selected, element, instance) { +        let label = 'Author'; +        const $item = instance.dropdown.find('.is-active'); +        if ($item.length) { +          label = $item.text(); +        } +        return label; +      }, +      clicked(options) { +        return options.e.preventDefault(); +      }, +      id(obj, element) { +        return $(element).data('id'); +      }, +    }); +  }); +} diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/job.js index 3d27a3544eb..c6b5844dff6 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/job.js @@ -1,15 +1,12 @@ -/* eslint-disable func-names, wrap-iife, no-use-before-define, -consistent-return, prefer-rest-params */  import _ from 'underscore';  import bp from './breakpoints';  import { bytesToKiB } from './lib/utils/number_utils';  import { setCiStatusFavicon } from './lib/utils/common_utils'; -window.Build = (function () { -  Build.timeout = null; -  Build.state = null; - -  function Build(options) { +export default class Job { +  constructor(options) { +    this.timeout = null; +    this.state = null;      this.options = options || $('.js-build-options').data();      this.pageUrl = this.options.pageUrl; @@ -19,9 +16,7 @@ window.Build = (function () {      this.$document = $(document);      this.logBytes = 0;      this.hasBeenScrolled = false; -      this.updateDropdown = this.updateDropdown.bind(this); -    this.getBuildTrace = this.getBuildTrace.bind(this);      this.$buildTrace = $('#build-trace');      this.$buildRefreshAnimation = $('.js-build-refresh'); @@ -33,7 +28,7 @@ window.Build = (function () {      this.$scrollTopBtn = $('.js-scroll-up');      this.$scrollBottomBtn = $('.js-scroll-down'); -    clearTimeout(Build.timeout); +    clearTimeout(this.timeout);      this.initSidebar();      this.populateJobs(this.buildStage); @@ -85,7 +80,7 @@ window.Build = (function () {      this.getBuildTrace();    } -  Build.prototype.initAffixTopArea = function () { +  initAffixTopArea() {      /**        If the browser does not support position sticky, it returns the position as static.        If the browser does support sticky, then we allow the browser to handle it, if not @@ -100,13 +95,14 @@ window.Build = (function () {          top: offsetTop,        },      }); -  }; +  } -  Build.prototype.canScroll = function () { +  // eslint-disable-next-line class-methods-use-this +  canScroll() {      return $(document).height() > $(window).height(); -  }; +  } -  Build.prototype.toggleScroll = function () { +  toggleScroll() {      const currentPosition = $(document).scrollTop();      const scrollHeight = $(document).height(); @@ -119,7 +115,7 @@ window.Build = (function () {          this.toggleDisableButton(this.$scrollTopBtn, false);          this.toggleDisableButton(this.$scrollBottomBtn, false);        } else if (currentPosition === 0) { -        // User is at Top of Build Log +        // User is at Top of  Log          this.toggleDisableButton(this.$scrollTopBtn, true);          this.toggleDisableButton(this.$scrollBottomBtn, false); @@ -133,38 +129,40 @@ window.Build = (function () {        this.toggleDisableButton(this.$scrollTopBtn, true);        this.toggleDisableButton(this.$scrollBottomBtn, true);      } -  }; +  } -  Build.prototype.scrollDown = function () { +  // eslint-disable-next-line class-methods-use-this +  scrollDown() {      $(document).scrollTop($(document).height()); -  }; +  } -  Build.prototype.scrollToBottom = function () { +  scrollToBottom() {      this.scrollDown();      this.hasBeenScrolled = true;      this.toggleScroll(); -  }; +  } -  Build.prototype.scrollToTop = function () { +  scrollToTop() {      $(document).scrollTop(0);      this.hasBeenScrolled = true;      this.toggleScroll(); -  }; +  } -  Build.prototype.toggleDisableButton = function ($button, disable) { +  // eslint-disable-next-line class-methods-use-this +  toggleDisableButton($button, disable) {      if (disable && $button.prop('disabled')) return;      $button.prop('disabled', disable); -  }; +  } -  Build.prototype.toggleScrollAnimation = function (toggle) { +  toggleScrollAnimation(toggle) {      this.$scrollBottomBtn.toggleClass('animate', toggle); -  }; +  } -  Build.prototype.initSidebar = function () { +  initSidebar() {      this.$sidebar = $('.js-build-sidebar'); -  }; +  } -  Build.prototype.getBuildTrace = function () { +  getBuildTrace() {      return $.ajax({        url: `${this.pageUrl}/trace.json`,        data: { state: this.state }, @@ -204,7 +202,7 @@ window.Build = (function () {              this.toggleScrollAnimation(false);            } -          Build.timeout = setTimeout(() => { +          this.timeout = setTimeout(() => {              this.getBuildTrace();            }, 4000);          } else { @@ -225,14 +223,14 @@ window.Build = (function () {          }        })        .then(() => this.toggleScroll()); -  }; - -  Build.prototype.shouldHideSidebarForViewport = function () { +  } +  // eslint-disable-next-line class-methods-use-this +  shouldHideSidebarForViewport() {      const bootstrapBreakpoint = bp.getBreakpointSize();      return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; -  }; +  } -  Build.prototype.toggleSidebar = function (shouldHide) { +  toggleSidebar(shouldHide) {      const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;      const $toggleButton = $('.js-sidebar-build-toggle-header'); @@ -249,17 +247,17 @@ window.Build = (function () {      } else {        $toggleButton.removeClass('hidden');      } -  }; +  } -  Build.prototype.sidebarOnResize = function () { +  sidebarOnResize() {      this.toggleSidebar(this.shouldHideSidebarForViewport()); -  }; +  } -  Build.prototype.sidebarOnClick = function () { +  sidebarOnClick() {      if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); -  }; - -  Build.prototype.updateArtifactRemoveDate = function () { +  } +  // eslint-disable-next-line class-methods-use-this, consistent-return +  updateArtifactRemoveDate() {      const $date = $('.js-artifacts-remove');      if ($date.length) {        const date = $date.text(); @@ -267,23 +265,21 @@ window.Build = (function () {          gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '),        );      } -  }; - -  Build.prototype.populateJobs = function (stage) { +  } +  // eslint-disable-next-line class-methods-use-this +  populateJobs(stage) {      $('.build-job').hide();      $(`.build-job[data-stage="${stage}"]`).show(); -  }; - -  Build.prototype.updateStageDropdownText = function (stage) { +  } +  // eslint-disable-next-line class-methods-use-this +  updateStageDropdownText(stage) {      $('.stage-selection').text(stage); -  }; +  } -  Build.prototype.updateDropdown = function (e) { +  updateDropdown(e) {      e.preventDefault();      const stage = e.currentTarget.text;      this.updateStageDropdownText(stage);      this.populateJobs(stage); -  }; - -  return Build; -})(); +  } +} diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index 3f6f40d47ba..6d671845f8e 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -43,16 +43,6 @@              type: 'link',            });          } - -        if (this.job.retry_path) { -          actions.push({ -            label: 'Retry', -            path: this.job.retry_path, -            cssClass: 'js-retry-button btn btn-inverted-secondary visible-md-block visible-lg-block', -            type: 'ujs-link', -          }); -        } -          return actions;        },      }, diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index f92e669414a..baaf5641200 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -1,5 +1,3 @@ -/* global Flash */ -  import Vue from 'vue';  import JobMediator from './job_details_mediator';  import jobHeader from './components/header.vue'; diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js index cc014b815c4..3e2658f9fc1 100644 --- a/app/assets/javascripts/jobs/job_details_mediator.js +++ b/app/assets/javascripts/jobs/job_details_mediator.js @@ -1,11 +1,12 @@ -/* global Flash */  /* global Build */  import Visibility from 'visibilityjs'; +import Flash from '../flash';  import Poll from '../lib/utils/poll';  import JobStore from './stores/job_store';  import JobService from './services/job_service'; -import '../build'; +import Job from '../job'; +import handleRevealVariables from '../build_variables';  export default class JobMediator {    constructor(options = {}) { @@ -20,7 +21,8 @@ export default class JobMediator {    }    initBuildClass() { -    this.build = new Build(); +    this.build = new Job(); +    handleRevealVariables();    }    fetchJob() { diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index d8814802d9e..c929dc98c10 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -1,124 +1,121 @@  /* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ -/* global Flash */  /* global Sortable */ -((global) => { -  class LabelManager { -    constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { -      this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority'); -      this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels'); -      this.otherLabels = otherLabels || $('.js-other-labels'); -      this.errorMessage = 'Unable to update label prioritization at this time'; -      this.emptyState = document.querySelector('#js-priority-labels-empty-state'); -      this.sortable = Sortable.create(this.prioritizedLabels.get(0), { -        filter: '.empty-message', -        forceFallback: true, -        fallbackClass: 'is-dragging', -        dataIdAttr: 'data-id', -        onUpdate: this.onPrioritySortUpdate.bind(this), -      }); -      this.bindEvents(); -    } +import Flash from './flash'; -    bindEvents() { -      this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick); -      return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); -    } +export default class LabelManager { +  constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { +    this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority'); +    this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels'); +    this.otherLabels = otherLabels || $('.js-other-labels'); +    this.errorMessage = 'Unable to update label prioritization at this time'; +    this.emptyState = document.querySelector('#js-priority-labels-empty-state'); +    this.sortable = Sortable.create(this.prioritizedLabels.get(0), { +      filter: '.empty-message', +      forceFallback: true, +      fallbackClass: 'is-dragging', +      dataIdAttr: 'data-id', +      onUpdate: this.onPrioritySortUpdate.bind(this), +    }); +    this.bindEvents(); +  } -    onTogglePriorityClick(e) { -      e.preventDefault(); -      const _this = e.data; -      const $btn = $(e.currentTarget); -      const $label = $(`#${$btn.data('domId')}`); -      const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; -      const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); -      $tooltip.tooltip('destroy'); -      _this.toggleLabelPriority($label, action); -      _this.toggleEmptyState($label, $btn, action); -    } +  bindEvents() { +    this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick); +    return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); +  } -    onButtonActionClick(e) { -      e.stopPropagation(); -      $(e.currentTarget).tooltip('hide'); -    } +  onTogglePriorityClick(e) { +    e.preventDefault(); +    const _this = e.data; +    const $btn = $(e.currentTarget); +    const $label = $(`#${$btn.data('domId')}`); +    const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; +    const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); +    $tooltip.tooltip('destroy'); +    _this.toggleLabelPriority($label, action); +    _this.toggleEmptyState($label, $btn, action); +  } -    toggleEmptyState($label, $btn, action) { -      this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li')); -    } +  onButtonActionClick(e) { +    e.stopPropagation(); +    $(e.currentTarget).tooltip('hide'); +  } -    toggleLabelPriority($label, action, persistState) { -      if (persistState == null) { -        persistState = true; -      } -      let xhr; -      const _this = this; -      const url = $label.find('.js-toggle-priority').data('url'); -      let $target = this.prioritizedLabels; -      let $from = this.otherLabels; -      if (action === 'remove') { -        $target = this.otherLabels; -        $from = this.prioritizedLabels; -      } -      $label.detach().appendTo($target); -      if ($from.find('li').length) { -        $from.find('.empty-message').removeClass('hidden'); -      } -      if ($target.find('> li:not(.empty-message)').length) { -        $target.find('.empty-message').addClass('hidden'); -      } -      // Return if we are not persisting state -      if (!persistState) { -        return; -      } -      if (action === 'remove') { -        xhr = $.ajax({ -          url, -          type: 'DELETE' -        }); -        // Restore empty message -        if (!$from.find('li').length) { -          $from.find('.empty-message').removeClass('hidden'); -        } -      } else { -        xhr = this.savePrioritySort($label, action); -      } -      return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); -    } +  toggleEmptyState($label, $btn, action) { +    this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li')); +  } -    onPrioritySortUpdate() { -      const xhr = this.savePrioritySort(); -      return xhr.fail(function() { -        return new Flash(this.errorMessage, 'alert'); -      }); +  toggleLabelPriority($label, action, persistState) { +    if (persistState == null) { +      persistState = true;      } - -    savePrioritySort() { -      return $.post({ -        url: this.prioritizedLabels.data('url'), -        data: { -          label_ids: this.getSortedLabelsIds() -        } +    let xhr; +    const _this = this; +    const url = $label.find('.js-toggle-priority').data('url'); +    let $target = this.prioritizedLabels; +    let $from = this.otherLabels; +    if (action === 'remove') { +      $target = this.otherLabels; +      $from = this.prioritizedLabels; +    } +    $label.detach().appendTo($target); +    if ($from.find('li').length) { +      $from.find('.empty-message').removeClass('hidden'); +    } +    if ($target.find('> li:not(.empty-message)').length) { +      $target.find('.empty-message').addClass('hidden'); +    } +    // Return if we are not persisting state +    if (!persistState) { +      return; +    } +    if (action === 'remove') { +      xhr = $.ajax({ +        url, +        type: 'DELETE'        }); +      // Restore empty message +      if (!$from.find('li').length) { +        $from.find('.empty-message').removeClass('hidden'); +      } +    } else { +      xhr = this.savePrioritySort($label, action);      } +    return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); +  } -    rollbackLabelPosition($label, originalAction) { -      const action = originalAction === 'remove' ? 'add' : 'remove'; -      this.toggleLabelPriority($label, action, false); +  onPrioritySortUpdate() { +    const xhr = this.savePrioritySort(); +    return xhr.fail(function() {        return new Flash(this.errorMessage, 'alert'); -    } +    }); +  } -    getSortedLabelsIds() { -      const sortedIds = []; -      this.prioritizedLabels.find('> li').each(function() { -        const id = $(this).data('id'); +  savePrioritySort() { +    return $.post({ +      url: this.prioritizedLabels.data('url'), +      data: { +        label_ids: this.getSortedLabelsIds() +      } +    }); +  } -        if (id) { -          sortedIds.push(id); -        } -      }); -      return sortedIds; -    } +  rollbackLabelPosition($label, originalAction) { +    const action = originalAction === 'remove' ? 'add' : 'remove'; +    this.toggleLabelPriority($label, action, false); +    return new Flash(this.errorMessage, 'alert');    } -  gl.LabelManager = LabelManager; -})(window.gl || (window.gl = {})); +  getSortedLabelsIds() { +    const sortedIds = []; +    this.prioritizedLabels.find('> li').each(function() { +      const id = $(this).data('id'); + +      if (id) { +        sortedIds.push(id); +      } +    }); +    return sortedIds; +  } +} diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js index 03dd61b4263..7aab13ed9c6 100644 --- a/app/assets/javascripts/labels.js +++ b/app/assets/javascripts/labels.js @@ -1,44 +1,35 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, max-len */ -(function() { -  this.Labels = (function() { -    function Labels() { -      this.setSuggestedColor = this.setSuggestedColor.bind(this); -      this.updateColorPreview = this.updateColorPreview.bind(this); -      var form; -      form = $('.label-form'); -      this.cleanBinding(); -      this.addBinding(); -      this.updateColorPreview(); -    } +export default class Labels { +  constructor() { +    this.setSuggestedColor = this.setSuggestedColor.bind(this); +    this.updateColorPreview = this.updateColorPreview.bind(this); +    this.cleanBinding(); +    this.addBinding(); +    this.updateColorPreview(); +  } -    Labels.prototype.addBinding = function() { -      $(document).on('click', '.suggest-colors a', this.setSuggestedColor); -      return $(document).on('input', 'input#label_color', this.updateColorPreview); -    }; +  addBinding() { +    $(document).on('click', '.suggest-colors a', this.setSuggestedColor); +    return $(document).on('input', 'input#label_color', this.updateColorPreview); +  } +  // eslint-disable-next-line class-methods-use-this +  cleanBinding() { +    $(document).off('click', '.suggest-colors a'); +    return $(document).off('input', 'input#label_color'); +  } +  // eslint-disable-next-line class-methods-use-this +  updateColorPreview() { +    const previewColor = $('input#label_color').val(); +    return $('div.label-color-preview').css('background-color', previewColor); +  // Updates the the preview color with the hex-color input +  } -    Labels.prototype.cleanBinding = function() { -      $(document).off('click', '.suggest-colors a'); -      return $(document).off('input', 'input#label_color'); -    }; - -    Labels.prototype.updateColorPreview = function() { -      var previewColor; -      previewColor = $('input#label_color').val(); -      return $('div.label-color-preview').css('background-color', previewColor); -    // Updates the the preview color with the hex-color input -    }; - -    // Updates the preview color with a click on a suggested color -    Labels.prototype.setSuggestedColor = function(e) { -      var color; -      color = $(e.currentTarget).data('color'); -      $('input#label_color').val(color); -      this.updateColorPreview(); -      // Notify the form, that color has changed -      $('.label-form').trigger('keyup'); -      return e.preventDefault(); -    }; - -    return Labels; -  })(); -}).call(window); +  // Updates the preview color with a click on a suggested color +  setSuggestedColor(e) { +    const color = $(e.currentTarget).data('color'); +    $('input#label_color').val(color); +    this.updateColorPreview(); +    // Notify the form, that color has changed +    $('.label-form').trigger('keyup'); +    return e.preventDefault(); +  } +} diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index d479f7ed682..f7a1c9f1e40 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -6,474 +6,475 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';  import DropdownUtils from './filtered_search/dropdown_utils';  import CreateLabelDropdown from './create_label'; -(function() { -  this.LabelsSelect = (function() { -    function LabelsSelect(els) { -      var _this, $els; -      _this = this; +export default class LabelsSelect { +  constructor(els, options = {}) { +    var _this, $els; +    _this = this; -      $els = $(els); +    $els = $(els); -      if (!els) { -        $els = $('.js-label-select'); -      } +    if (!els) { +      $els = $('.js-label-select'); +    } -      $els.each(function(i, dropdown) { -        var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; -        $dropdown = $(dropdown); -        $dropdownContainer = $dropdown.closest('.labels-filter'); -        $toggleText = $dropdown.find('.dropdown-toggle-text'); -        namespacePath = $dropdown.data('namespace-path'); -        projectPath = $dropdown.data('project-path'); -        labelUrl = $dropdown.data('labels'); -        issueUpdateURL = $dropdown.data('issueUpdate'); -        selectedLabel = $dropdown.data('selected'); -        if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) { -          selectedLabel = selectedLabel.split(','); -        } -        showNo = $dropdown.data('show-no'); -        showAny = $dropdown.data('show-any'); -        showMenuAbove = $dropdown.data('showMenuAbove'); -        defaultLabel = $dropdown.data('default-label'); -        abilityName = $dropdown.data('ability-name'); -        $selectbox = $dropdown.closest('.selectbox'); -        $block = $selectbox.closest('.block'); -        $form = $dropdown.closest('form, .js-issuable-update'); -        $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span'); -        $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); -        $value = $block.find('.value'); -        $loading = $block.find('.block-loading').fadeOut(); -        fieldName = $dropdown.data('field-name'); -        useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown'); -        propertyName = useId ? 'id' : 'title'; -        initialSelected = $selectbox -          .find('input[name="' + $dropdown.data('field-name') + '"]') -          .map(function () { -            return this.value; -          }).get(); -        if (issueUpdateURL != null) { -          issueURLSplit = issueUpdateURL.split('/'); -        } -        if (issueUpdateURL) { -          labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>'); -          labelNoneHTMLTemplate = '<span class="no-value">None</span>'; -        } +    $els.each(function(i, dropdown) { +      var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; +      $dropdown = $(dropdown); +      $dropdownContainer = $dropdown.closest('.labels-filter'); +      $toggleText = $dropdown.find('.dropdown-toggle-text'); +      namespacePath = $dropdown.data('namespace-path'); +      projectPath = $dropdown.data('project-path'); +      labelUrl = $dropdown.data('labels'); +      issueUpdateURL = $dropdown.data('issueUpdate'); +      selectedLabel = $dropdown.data('selected'); +      if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) { +        selectedLabel = selectedLabel.split(','); +      } +      showNo = $dropdown.data('show-no'); +      showAny = $dropdown.data('show-any'); +      showMenuAbove = $dropdown.data('showMenuAbove'); +      defaultLabel = $dropdown.data('default-label'); +      abilityName = $dropdown.data('ability-name'); +      $selectbox = $dropdown.closest('.selectbox'); +      $block = $selectbox.closest('.block'); +      $form = $dropdown.closest('form, .js-issuable-update'); +      $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span'); +      $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); +      $value = $block.find('.value'); +      $loading = $block.find('.block-loading').fadeOut(); +      fieldName = $dropdown.data('field-name'); +      useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown'); +      propertyName = useId ? 'id' : 'title'; +      initialSelected = $selectbox +        .find('input[name="' + $dropdown.data('field-name') + '"]') +        .map(function () { +          return this.value; +        }).get(); +      if (issueUpdateURL != null) { +        issueURLSplit = issueUpdateURL.split('/'); +      } +      if (issueUpdateURL) { +        labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>'); +        labelNoneHTMLTemplate = '<span class="no-value">None</span>'; +      } +      const handleClick = options.handleClick; -        $sidebarLabelTooltip.tooltip(); +      $sidebarLabelTooltip.tooltip(); -        if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { -          new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath); -        } +      if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { +        new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath); +      } -        saveLabelData = function() { -          var data, selected; -          selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() { -            return this.value; -          }).get(); +      saveLabelData = function() { +        var data, selected; +        selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() { +          return this.value; +        }).get(); -          if (_.isEqual(initialSelected, selected)) return; -          initialSelected = selected; +        if (_.isEqual(initialSelected, selected)) return; +        initialSelected = selected; -          data = {}; -          data[abilityName] = {}; -          data[abilityName].label_ids = selected; -          if (!selected.length) { -            data[abilityName].label_ids = ['']; +        data = {}; +        data[abilityName] = {}; +        data[abilityName].label_ids = selected; +        if (!selected.length) { +          data[abilityName].label_ids = ['']; +        } +        $loading.removeClass('hidden').fadeIn(); +        $dropdown.trigger('loading.gl.dropdown'); +        return $.ajax({ +          type: 'PUT', +          url: issueUpdateURL, +          dataType: 'JSON', +          data: data +        }).done(function(data) { +          var labelCount, template, labelTooltipTitle, labelTitles; +          $loading.fadeOut(); +          $dropdown.trigger('loaded.gl.dropdown'); +          $selectbox.hide(); +          data.issueURLSplit = issueURLSplit; +          labelCount = 0; +          if (data.labels.length) { +            template = labelHTMLTemplate(data); +            labelCount = data.labels.length;            } -          $loading.removeClass('hidden').fadeIn(); -          $dropdown.trigger('loading.gl.dropdown'); -          return $.ajax({ -            type: 'PUT', -            url: issueUpdateURL, -            dataType: 'JSON', -            data: data -          }).done(function(data) { -            var labelCount, template, labelTooltipTitle, labelTitles; -            $loading.fadeOut(); -            $dropdown.trigger('loaded.gl.dropdown'); -            $selectbox.hide(); -            data.issueURLSplit = issueURLSplit; -            labelCount = 0; -            if (data.labels.length) { -              template = labelHTMLTemplate(data); -              labelCount = data.labels.length; -            } -            else { -              template = labelNoneHTMLTemplate; -            } -            $value.removeAttr('style').html(template); -            $sidebarCollapsedValue.text(labelCount); - -            if (data.labels.length) { -              labelTitles = data.labels.map(function(label) { -                return label.title; -              }); +          else { +            template = labelNoneHTMLTemplate; +          } +          $value.removeAttr('style').html(template); +          $sidebarCollapsedValue.text(labelCount); -              if (labelTitles.length > 5) { -                labelTitles = labelTitles.slice(0, 5); -                labelTitles.push('and ' + (data.labels.length - 5) + ' more'); -              } +          if (data.labels.length) { +            labelTitles = data.labels.map(function(label) { +              return label.title; +            }); -              labelTooltipTitle = labelTitles.join(', '); -            } -            else { -              labelTooltipTitle = ''; -              $sidebarLabelTooltip.tooltip('destroy'); +            if (labelTitles.length > 5) { +              labelTitles = labelTitles.slice(0, 5); +              labelTitles.push('and ' + (data.labels.length - 5) + ' more');              } -            $sidebarLabelTooltip -              .attr('title', labelTooltipTitle) -              .tooltip('fixTitle'); +            labelTooltipTitle = labelTitles.join(', '); +          } +          else { +            labelTooltipTitle = ''; +            $sidebarLabelTooltip.tooltip('destroy'); +          } -            $('.has-tooltip', $value).tooltip({ -              container: 'body' -            }); +          $sidebarLabelTooltip +            .attr('title', labelTooltipTitle) +            .tooltip('fixTitle'); + +          $('.has-tooltip', $value).tooltip({ +            container: 'body'            }); -        }; -        $dropdown.glDropdown({ -          showMenuAbove: showMenuAbove, -          data: function(term, callback) { -            return $.ajax({ -              url: labelUrl -            }).done(function(data) { -              data = _.chain(data).groupBy(function(label) { -                return label.title; -              }).map(function(label) { -                var color; -                color = _.map(label, function(dup) { -                  return dup.color; +        }); +      }; +      $dropdown.glDropdown({ +        showMenuAbove: showMenuAbove, +        data: function(term, callback) { +          return $.ajax({ +            url: labelUrl +          }).done(function(data) { +            data = _.chain(data).groupBy(function(label) { +              return label.title; +            }).map(function(label) { +              var color; +              color = _.map(label, function(dup) { +                return dup.color; +              }); +              return { +                id: label[0].id, +                title: label[0].title, +                color: color, +                duplicate: color.length > 1 +              }; +            }).value(); +            if ($dropdown.hasClass('js-extra-options')) { +              var extraData = []; +              if (showNo) { +                extraData.unshift({ +                  id: 0, +                  title: 'No Label'                  }); -                return { -                  id: label[0].id, -                  title: label[0].title, -                  color: color, -                  duplicate: color.length > 1 -                }; -              }).value(); -              if ($dropdown.hasClass('js-extra-options')) { -                var extraData = []; -                if (showNo) { -                  extraData.unshift({ -                    id: 0, -                    title: 'No Label' -                  }); -                } -                if (showAny) { -                  extraData.unshift({ -                    isAny: true, -                    title: 'Any Label' -                  }); -                } -                if (extraData.length) { -                  extraData.push('divider'); -                  data = extraData.concat(data); -                } -              } - -              callback(data); -              if (showMenuAbove) { -                $dropdown.data('glDropdown').positionMenuAbove(); -              } -            }); -          }, -          renderRow: function(label, instance) { -            var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; -            $li = $('<li>'); -            $a = $('<a href="#">'); -            selectedClass = []; -            removesAll = label.id <= 0 || (label.id == null); -            if ($dropdown.hasClass('js-filter-bulk-update')) { -              indeterminate = $dropdown.data('indeterminate') || []; -              marked = $dropdown.data('marked') || []; - -              if (indeterminate.indexOf(label.id) !== -1) { -                selectedClass.push('is-indeterminate');                } - -              if (marked.indexOf(label.id) !== -1) { -                // Remove is-indeterminate class if the item will be marked as active -                i = selectedClass.indexOf('is-indeterminate'); -                if (i !== -1) { -                  selectedClass.splice(i, 1); -                } -                selectedClass.push('is-active'); +              if (showAny) { +                extraData.unshift({ +                  isAny: true, +                  title: 'Any Label' +                });                } -            } else { -              if (this.id(label)) { -                dropdownName = $dropdown.data('fieldName'); -                dropdownValue = this.id(label).toString().replace(/'/g, '\\\''); - -                if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) { -                  selectedClass.push('is-active'); -                } +              if (extraData.length) { +                extraData.push('divider'); +                data = extraData.concat(data);                } +            } -              if ($dropdown.hasClass('js-multiselect') && removesAll) { -                selectedClass.push('dropdown-clear-active'); -              } +            callback(data); +            if (showMenuAbove) { +              $dropdown.data('glDropdown').positionMenuAbove();              } -            if (label.duplicate) { -              color = gl.DropdownUtils.duplicateLabelColor(label.color); +          }); +        }, +        renderRow: function(label, instance) { +          var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; +          $li = $('<li>'); +          $a = $('<a href="#">'); +          selectedClass = []; +          removesAll = label.id <= 0 || (label.id == null); +          if ($dropdown.hasClass('js-filter-bulk-update')) { +            indeterminate = $dropdown.data('indeterminate') || []; +            marked = $dropdown.data('marked') || []; + +            if (indeterminate.indexOf(label.id) !== -1) { +              selectedClass.push('is-indeterminate');              } -            else { -              if (label.color != null) { -                color = label.color[0]; + +            if (marked.indexOf(label.id) !== -1) { +              // Remove is-indeterminate class if the item will be marked as active +              i = selectedClass.indexOf('is-indeterminate'); +              if (i !== -1) { +                selectedClass.splice(i, 1);                } +              selectedClass.push('is-active');              } -            if (color) { -              colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>"; -            } -            else { -              colorEl = ''; -            } -            // We need to identify which items are actually labels -            if (label.id) { -              selectedClass.push('label-item'); -              $a.attr('data-label-id', label.id); -            } -            $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title); -            // Return generated html -            return $li.html($a).prop('outerHTML'); -          }, -          search: { -            fields: ['title'] -          }, -          selectable: true, -          filterable: true, -          selected: $dropdown.data('selected') || [], -          toggleLabel: function(selected, el) { -            var isSelected = el !== null ? el.hasClass('is-active') : false; -            var title = selected.title; -            var selectedLabels = this.selected; - -            if (selected.id === 0) { -              this.selected = []; -              return 'No Label'; +          } else { +            if (this.id(label)) { +              dropdownName = $dropdown.data('fieldName'); +              dropdownValue = this.id(label).toString().replace(/'/g, '\\\''); + +              if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) { +                selectedClass.push('is-active'); +              }              } -            else if (isSelected) { -              this.selected.push(title); + +            if ($dropdown.hasClass('js-multiselect') && removesAll) { +              selectedClass.push('dropdown-clear-active');              } -            else { -              var index = this.selected.indexOf(title); -              this.selected.splice(index, 1); +          } +          if (label.duplicate) { +            color = gl.DropdownUtils.duplicateLabelColor(label.color); +          } +          else { +            if (label.color != null) { +              color = label.color[0];              } +          } +          if (color) { +            colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>"; +          } +          else { +            colorEl = ''; +          } +          // We need to identify which items are actually labels +          if (label.id) { +            selectedClass.push('label-item'); +            $a.attr('data-label-id', label.id); +          } +          $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title); +          // Return generated html +          return $li.html($a).prop('outerHTML'); +        }, +        search: { +          fields: ['title'] +        }, +        selectable: true, +        filterable: true, +        selected: $dropdown.data('selected') || [], +        toggleLabel: function(selected, el) { +          var isSelected = el !== null ? el.hasClass('is-active') : false; +          var title = selected.title; +          var selectedLabels = this.selected; + +          if (selected.id === 0) { +            this.selected = []; +            return 'No Label'; +          } +          else if (isSelected) { +            this.selected.push(title); +          } +          else { +            var index = this.selected.indexOf(title); +            this.selected.splice(index, 1); +          } + +          if (selectedLabels.length === 1) { +            return selectedLabels; +          } +          else if (selectedLabels.length) { +            return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more"; +          } +          else { +            return defaultLabel; +          } +        }, +        fieldName: $dropdown.data('field-name'), +        id: function(label) { +          if (label.id <= 0) return label.title; + +          if ($dropdown.hasClass('js-issuable-form-dropdown')) { +            return label.id; +          } -            if (selectedLabels.length === 1) { -              return selectedLabels; +          if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) { +            return label.title; +          } +          else { +            return label.id; +          } +        }, +        hidden: function() { +          var isIssueIndex, isMRIndex, page, selectedLabels; +          page = $('body').attr('data-page'); +          isIssueIndex = page === 'projects:issues:index'; +          isMRIndex = page === 'projects:merge_requests:index'; +          $selectbox.hide(); +          // display:block overrides the hide-collapse rule +          $value.removeAttr('style'); + +          if ($dropdown.hasClass('js-issuable-form-dropdown')) { +            return; +          } + +          if ($('html').hasClass('issue-boards-page')) { +            return; +          } +          if ($dropdown.hasClass('js-multiselect')) { +            if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { +              selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']"); +              Issuable.filterResults($dropdown.closest('form'));              } -            else if (selectedLabels.length) { -              return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more"; +            else if ($dropdown.hasClass('js-filter-submit')) { +              $dropdown.closest('form').submit();              }              else { -              return defaultLabel; +              if (!$dropdown.hasClass('js-filter-bulk-update')) { +                saveLabelData(); +              }              } -          }, -          fieldName: $dropdown.data('field-name'), -          id: function(label) { -            if (label.id <= 0) return label.title; +          } +        }, +        multiSelect: $dropdown.hasClass('js-multiselect'), +        vue: $dropdown.hasClass('js-issue-board-sidebar'), +        clicked: function(options) { +          const { $el, e, isMarking } = options; +          const label = options.selectedObj; + +          var isIssueIndex, isMRIndex, page, boardsModel; +          var fadeOutLoader = () => { +            $loading.fadeOut(); +          }; -            if ($dropdown.hasClass('js-issuable-form-dropdown')) { -              return label.id; -            } +          page = $('body').attr('data-page'); +          isIssueIndex = page === 'projects:issues:index'; +          isMRIndex = page === 'projects:merge_requests:index'; -            if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) { -              return label.title; -            } -            else { -              return label.id; -            } -          }, -          hidden: function() { -            var isIssueIndex, isMRIndex, page, selectedLabels; -            page = $('body').data('page'); -            isIssueIndex = page === 'projects:issues:index'; -            isMRIndex = page === 'projects:merge_requests:index'; -            $selectbox.hide(); -            // display:block overrides the hide-collapse rule -            $value.removeAttr('style'); - -            if ($dropdown.hasClass('js-issuable-form-dropdown')) { -              return; -            } +          if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { +            $dropdown.parent() +              .find('.dropdown-clear-active') +              .removeClass('is-active'); +          } -            if ($('html').hasClass('issue-boards-page')) { -              return; -            } -            if ($dropdown.hasClass('js-multiselect')) { -              if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { -                selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']"); -                Issuable.filterResults($dropdown.closest('form')); -              } -              else if ($dropdown.hasClass('js-filter-submit')) { -                $dropdown.closest('form').submit(); -              } -              else { -                if (!$dropdown.hasClass('js-filter-bulk-update')) { -                  saveLabelData(); -                } -              } -            } -          }, -          multiSelect: $dropdown.hasClass('js-multiselect'), -          vue: $dropdown.hasClass('js-issue-board-sidebar'), -          clicked: function(options) { -            const { $el, e, isMarking } = options; -            const label = options.selectedObj; - -            var isIssueIndex, isMRIndex, page, boardsModel; -            var fadeOutLoader = () => { -              $loading.fadeOut(); -            }; - -            page = $('body').data('page'); -            isIssueIndex = page === 'projects:issues:index'; -            isMRIndex = page === 'projects:merge_requests:index'; - -            if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { -              $dropdown.parent() -                .find('.dropdown-clear-active') -                .removeClass('is-active'); -            } +          if ($dropdown.hasClass('js-issuable-form-dropdown')) { +            return; +          } -            if ($dropdown.hasClass('js-issuable-form-dropdown')) { -              return; -            } +          if ($dropdown.hasClass('js-filter-bulk-update')) { +            _this.enableBulkLabelDropdown(); +            _this.setDropdownData($dropdown, isMarking, label.id); +            return; +          } -            if ($dropdown.hasClass('js-filter-bulk-update')) { -              _this.enableBulkLabelDropdown(); -              _this.setDropdownData($dropdown, isMarking, label.id); -              return; -            } +          if ($dropdown.closest('.add-issues-modal').length) { +            boardsModel = gl.issueBoards.ModalStore.store.filter; +          } -            if ($dropdown.closest('.add-issues-modal').length) { -              boardsModel = gl.issueBoards.ModalStore.store.filter; +          if (boardsModel) { +            if (label.isAny) { +              boardsModel['label_name'] = []; +            } else if ($el.hasClass('is-active')) { +              boardsModel['label_name'].push(label.title);              } -            if (boardsModel) { -              if (label.isAny) { -                boardsModel['label_name'] = []; -              } else if ($el.hasClass('is-active')) { -                boardsModel['label_name'].push(label.title); -              } - -              e.preventDefault(); -              return; +            e.preventDefault(); +            return; +          } +          else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { +            if (!$dropdown.hasClass('js-multiselect')) { +              selectedLabel = label.title; +              return Issuable.filterResults($dropdown.closest('form'));              } -            else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { -              if (!$dropdown.hasClass('js-multiselect')) { -                selectedLabel = label.title; -                return Issuable.filterResults($dropdown.closest('form')); -              } +          } +          else if ($dropdown.hasClass('js-filter-submit')) { +            return $dropdown.closest('form').submit(); +          } +          else if ($dropdown.hasClass('js-issue-board-sidebar')) { +            if ($el.hasClass('is-active')) { +              gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({ +                id: label.id, +                title: label.title, +                color: label.color[0], +                textColor: '#fff' +              }));              } -            else if ($dropdown.hasClass('js-filter-submit')) { -              return $dropdown.closest('form').submit(); +            else { +              var labels = gl.issueBoards.BoardsStore.detail.issue.labels; +              labels = labels.filter(function (selectedLabel) { +                return selectedLabel.id !== label.id; +              }); +              gl.issueBoards.BoardsStore.detail.issue.labels = labels;              } -            else if ($dropdown.hasClass('js-issue-board-sidebar')) { -              if ($el.hasClass('is-active')) { -                gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({ -                  id: label.id, -                  title: label.title, -                  color: label.color[0], -                  textColor: '#fff' -                })); -              } -              else { -                var labels = gl.issueBoards.BoardsStore.detail.issue.labels; -                labels = labels.filter(function (selectedLabel) { -                  return selectedLabel.id !== label.id; -                }); -                gl.issueBoards.BoardsStore.detail.issue.labels = labels; -              } -              $loading.fadeIn(); +            $loading.fadeIn(); + +            gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) +              .then(fadeOutLoader) +              .catch(fadeOutLoader); +          } +          else if (handleClick) { +            e.preventDefault(); +            handleClick(label); +          } +          else { +            if ($dropdown.hasClass('js-multiselect')) { -              gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) -                .then(fadeOutLoader) -                .catch(fadeOutLoader);              }              else { -              if ($dropdown.hasClass('js-multiselect')) { - -              } -              else { -                return saveLabelData(); -              } +              return saveLabelData();              } -          }, -        }); - -        // Set dropdown data -        _this.setOriginalDropdownData($dropdownContainer, $dropdown); +          } +        },        }); -      this.bindEvents(); -    } -    LabelsSelect.prototype.bindEvents = function() { -      return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue); -    }; - -    LabelsSelect.prototype.onSelectCheckboxIssue = function() { -      if ($('.selected_issue:checked').length) { -        return; +      // Set dropdown data +      _this.setOriginalDropdownData($dropdownContainer, $dropdown); +    }); +    this.bindEvents(); +  } + +  bindEvents() { +    return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue); +  } +  // eslint-disable-next-line class-methods-use-this +  onSelectCheckboxIssue() { +    if ($('.selected_issue:checked').length) { +      return; +    } +    return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label'); +  } +  // eslint-disable-next-line class-methods-use-this +  enableBulkLabelDropdown() { +    IssuableBulkUpdateActions.willUpdateLabels = true; +  } +  // eslint-disable-next-line class-methods-use-this +  setDropdownData($dropdown, isMarking, value) { +    var i, markedIds, unmarkedIds, indeterminateIds; + +    markedIds = $dropdown.data('marked') || []; +    unmarkedIds = $dropdown.data('unmarked') || []; +    indeterminateIds = $dropdown.data('indeterminate') || []; + +    if (isMarking) { +      markedIds.push(value); + +      i = indeterminateIds.indexOf(value); +      if (i > -1) { +        indeterminateIds.splice(i, 1);        } -      return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label'); -    }; -    LabelsSelect.prototype.enableBulkLabelDropdown = function() { -      IssuableBulkUpdateActions.willUpdateLabels = true; -    }; - -    LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) { -      var i, markedIds, unmarkedIds, indeterminateIds; - -      markedIds = $dropdown.data('marked') || []; -      unmarkedIds = $dropdown.data('unmarked') || []; -      indeterminateIds = $dropdown.data('indeterminate') || []; - -      if (isMarking) { -        markedIds.push(value); - -        i = indeterminateIds.indexOf(value); -        if (i > -1) { -          indeterminateIds.splice(i, 1); -        } - -        i = unmarkedIds.indexOf(value); -        if (i > -1) { -          unmarkedIds.splice(i, 1); -        } -      } else { -        // If marked item (not common) is unmarked -        i = markedIds.indexOf(value); -        if (i > -1) { -          markedIds.splice(i, 1); -        } - -        // If an indeterminate item is being unmarked -        if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) { -          unmarkedIds.push(value); -        } - -        // If a marked item is being unmarked -        // (a marked item could also be a label that is present in all selection) -        if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) { -          unmarkedIds.push(value); -        } +      i = unmarkedIds.indexOf(value); +      if (i > -1) { +        unmarkedIds.splice(i, 1); +      } +    } else { +      // If marked item (not common) is unmarked +      i = markedIds.indexOf(value); +      if (i > -1) { +        markedIds.splice(i, 1);        } -      $dropdown.data('marked', markedIds); -      $dropdown.data('unmarked', unmarkedIds); -      $dropdown.data('indeterminate', indeterminateIds); -    }; +      // If an indeterminate item is being unmarked +      if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) { +        unmarkedIds.push(value); +      } -    LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) { -      var labels = []; -      $container.find('[name="label_name[]"]').map(function() { -        return labels.push(this.value); -      }); -      $dropdown.data('marked', labels); -    }; +      // If a marked item is being unmarked +      // (a marked item could also be a label that is present in all selection) +      if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) { +        unmarkedIds.push(value); +      } +    } -    return LabelsSelect; -  })(); -}).call(window); +    $dropdown.data('marked', markedIds); +    $dropdown.data('unmarked', unmarkedIds); +    $dropdown.data('indeterminate', indeterminateIds); +  } +  // eslint-disable-next-line class-methods-use-this +  setOriginalDropdownData($container, $dropdown) { +    const labels = []; +    $container.find('[name="label_name[]"]').map(function() { +      return labels.push(this.value); +    }); +    $dropdown.data('marked', labels); +  } +} diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index d064a2c0024..a6f82b247e2 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -1,7 +1,7 @@  /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */  import _ from 'underscore';  import Cookies from 'js-cookie'; -import NewNavSidebar from './new_sidebar'; +import ContextualSidebar from './contextual_sidebar';  import initFlyOutNav from './fly_out_nav';  (function() { @@ -51,8 +51,8 @@ import initFlyOutNav from './fly_out_nav';    });    $(() => { -    const newNavSidebar = new NewNavSidebar(); -    newNavSidebar.bindEvents(); +    const contextualSidebar = new ContextualSidebar(); +    contextualSidebar.bindEvents();      initFlyOutNav();    }); diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js index 3d64b121fa7..dbbf1637a47 100644 --- a/app/assets/javascripts/lazy_loader.js +++ b/app/assets/javascripts/lazy_loader.js @@ -1,5 +1,3 @@ -/* eslint-disable one-export, one-var, one-var-declaration-per-line */ -  import _ from 'underscore';  export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; @@ -21,7 +19,10 @@ export default class LazyLoader {    }    searchLazyImages() {      this.lazyImages = [].slice.call(document.querySelectorAll('.lazy')); -    this.checkElementsInView(); + +    if (this.lazyImages.length) { +      this.checkElementsInView(); +    }    }    startContentObserver() {      const contentNode = document.querySelector(this.observerNode) || document.querySelector('body'); @@ -45,15 +46,13 @@ export default class LazyLoader {    checkElementsInView() {      const scrollTop = pageYOffset;      const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD; -    let imgBoundRect, imgTop, imgBound;      // Loading Images which are in the current viewport or close to them      this.lazyImages = this.lazyImages.filter((selectedImage) => {        if (selectedImage.getAttribute('data-src')) { -        imgBoundRect = selectedImage.getBoundingClientRect(); - -        imgTop = scrollTop + imgBoundRect.top; -        imgBound = imgTop + imgBoundRect.height; +        const imgBoundRect = selectedImage.getBoundingClientRect(); +        const imgTop = scrollTop + imgBoundRect.top; +        const imgBound = imgTop + imgBoundRect.height;          if (scrollTop < imgBound && visHeight > imgTop) {            LazyLoader.loadImage(selectedImage); diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js new file mode 100644 index 00000000000..45bff245827 --- /dev/null +++ b/app/assets/javascripts/lib/utils/axios_utils.js @@ -0,0 +1,6 @@ +import axios from 'axios'; +import csrf from './csrf'; + +export default function setAxiosCsrfToken() { +  axios.defaults.headers.common[csrf.headerKey] = csrf.token; +} diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 423a25fbdfa..07899777a1e 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,5 +1,5 @@ -export const getPagePath = (index = 0) => $('body').data('page').split(':')[index]; +export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];  export const isInGroupsPage = () => getPagePath() === 'groups'; @@ -403,7 +403,11 @@ export const setCiStatusFavicon = (pageUrl) => {    });  }; -export const spriteIcon = icon => `<svg><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`; +export const spriteIcon = (icon, className = '') => { +  const classAttribute = className.length > 0 ? `class="${className}"` : ''; + +  return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`; +};  export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`; diff --git a/app/assets/javascripts/lib/utils/datefix.js b/app/assets/javascripts/lib/utils/datefix.js index 990dc3f6d1a..e98c9068367 100644 --- a/app/assets/javascripts/lib/utils/datefix.js +++ b/app/assets/javascripts/lib/utils/datefix.js @@ -1,8 +1,29 @@ -const DateFix = { -  dashedFix(val) { -    const [y, m, d] = val.split('-'); -    return new Date(y, m - 1, d); -  }, + +export const pad = (val, len = 2) => (`0${val}`).slice(-len); + +/** + * Formats dates in Pickaday + * @param {String} dateString Date in yyyy-mm-dd format + * @return {Date} UTC format + */ +export const parsePikadayDate = (dateString) => { +  const parts = dateString.split('-'); +  const year = parseInt(parts[0], 10); +  const month = parseInt(parts[1] - 1, 10); +  const day = parseInt(parts[2], 10); + +  return new Date(year, month, day);  }; -export default DateFix; +/** + * Used `onSelect` method in pickaday + * @param {Date} date UTC format + * @return {String} Date formated in yyyy-mm-dd + */ +export const pikadayToString = (date) => { +  const day = pad(date.getDate()); +  const month = pad(date.getMonth() + 1); +  const year = date.getFullYear(); + +  return `${year}-${month}-${day}`; +}; diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index 64db42701ce..098afcfa1b4 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -28,14 +28,10 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {    }  }; -export default (el, insertPlaceholder = true) => { +export default (el, stickyTop, insertPlaceholder = true) => {    if (!el) return; -  const computedStyle = window.getComputedStyle(el); - -  if (!/sticky/.test(computedStyle.position)) return; - -  const stickyTop = parseInt(computedStyle.top, 10); +  if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return;    document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), {      passive: true, diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 021f936a4fa..f776829f69c 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */ +/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */  import 'vendor/latinise'; @@ -13,9 +13,17 @@ if ((base = w.gl).text == null) {  gl.text.addDelimiter = function(text) {    return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;  }; -gl.text.highCountTrim = function(count) { + +/** + * Returns '99+' for numbers bigger than 99. + * + * @param {Number} count + * @return {Number|String} + */ +export function highCountTrim(count) {    return count > 99 ? '99+' : count; -}; +} +  gl.text.randomString = function() {    return Math.random().toString(36).substring(7);  }; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 78c7a094127..1aa63216baf 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -85,7 +85,7 @@ w.gl.utils.getLocationHash = function(url) {    return hashIndex === -1 ? null : url.substring(hashIndex + 1);  }; -w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); +w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(window.location.href);  // eslint-disable-next-line import/prefer-default-export  export function visitUrl(url, external = false) { @@ -96,7 +96,7 @@ export function visitUrl(url, external = false) {      otherWindow.opener = null;      otherWindow.location = url;    } else { -    document.location.href = url; +    window.location.href = url;    }  } diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index 729baa2e1a7..3688a57937e 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,7 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */ - -(function() { -  window.addEventListener('beforeunload', function() { +export default function initLogoAnimation() { +  window.addEventListener('beforeunload', () => {      $('.tanuki-logo').addClass('animate');    }); -}).call(window); +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 853546b617b..31c5cfc5e55 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,5 +1,4 @@  /* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ -/* global Flash */  /* global ConfirmDangerModal */  /* global Aside */ @@ -13,7 +12,6 @@ import svg4everybody from 'svg4everybody';  // libraries with import side-effects  import 'mousetrap';  import 'mousetrap/plugins/pause/mousetrap-pause'; -import 'vendor/fuzzaldrin-plus';  // expose common libraries as globals (TODO: remove these)  window.jQuery = jQuery; @@ -22,15 +20,6 @@ window._ = _;  window.Dropzone = Dropzone;  window.Sortable = Sortable; -// shortcuts -import './shortcuts'; -import './shortcuts_blob'; -import './shortcuts_dashboard_navigation'; -import './shortcuts_navigation'; -import './shortcuts_find_file'; -import './shortcuts_issuable'; -import './shortcuts_network'; -  // templates  import './templates/issuable_template_selector';  import './templates/issuable_template_selectors'; @@ -47,59 +36,28 @@ import './lib/utils/url_utility';  // behaviors  import './behaviors/'; -// u2f -import './u2f/authenticate'; -import './u2f/error'; -import './u2f/register'; -import './u2f/util'; -  // everything else  import './activities';  import './admin'; -import './api';  import './aside'; -import './autosave';  import loadAwardsHandler from './awards_handler';  import bp from './breakpoints'; -import './broadcast_message'; -import './build'; -import './build_artifacts'; -import './build_variables'; -import './ci_lint_editor';  import './commits';  import './compare';  import './compare_autocomplete';  import './confirm_danger_modal'; -import './copy_as_gfm';  import './copy_to_clipboard'; -import './diff'; -import './dropzone_input'; -import './due_date_select'; -import './files_comment_button'; -import './flash'; +import Flash, { removeFlashClickListener } from './flash';  import './gl_dropdown';  import './gl_field_error';  import './gl_field_errors';  import './gl_form'; -import './group_avatar'; -import './group_label_subscription'; -import './groups_select'; -import './header'; -import './importer_status'; -import './issuable_index'; -import './issuable_context'; -import './issuable_form'; -import './issue'; -import './issue_status_select'; -import './label_manager'; -import './labels'; -import './labels_select'; +import initTodoToggle from './header'; +import initImporterStatus from './importer_status';  import './layout_nav';  import LazyLoader from './lazy_loader';  import './line_highlighter'; -import './logo'; -import './member_expiration_date'; -import './members'; +import initLogoAnimation from './logo';  import './merge_request';  import './merge_request_tabs';  import './milestone'; @@ -130,7 +88,6 @@ import './right_sidebar';  import './search';  import './search_autocomplete';  import './smart_interval'; -import './star';  import './subscription';  import './subscription_select';  import initBreadcrumbs from './breadcrumb'; @@ -169,11 +126,13 @@ $(function () {    var $document = $(document);    var $window = $(window);    var $sidebarGutterToggle = $('.js-sidebar-toggle'); -  var $flash = $('.flash-container');    var bootstrapBreakpoint = bp.getBreakpointSize();    var fitSidebarForSize;    initBreadcrumbs(); +  initImporterStatus(); +  initTodoToggle(); +  initLogoAnimation();    // Set the default path for all cookies to GitLab's root directory    Cookies.defaults.path = gon.relative_url_root || '/'; @@ -254,13 +213,6 @@ $(function () {    // Form submitter    });    gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); -  // Flash -  if ($flash.length > 0) { -    $flash.click(function () { -      return $(this).fadeOut(); -    }); -    $flash.show(); -  }    // Disable form buttons while a form is submitting    $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {      var buttons; @@ -362,4 +314,10 @@ $(function () {      event.preventDefault();      gl.utils.visitUrl(`${action}${$(this).serialize()}`);    }); + +  const flashContainer = document.querySelector('.flash-container'); + +  if (flashContainer && flashContainer.children.length) { +    removeFlashClickListener(flashContainer.children[0]); +  }  }); diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index cc9016e74da..84e70e35bad 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -1,55 +1,53 @@ -/* global dateFormat */ -  import Pikaday from 'pikaday'; - -(() => { -  // Add datepickers to all `js-access-expiration-date` elements. If those elements are -  // children of an element with the `clearable-input` class, and have a sibling -  // `js-clear-input` element, then show that element when there is a value in the -  // datepicker, and make clicking on that element clear the field. -  // -  window.gl = window.gl || {}; -  gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => { -    function toggleClearInput() { -      $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); -    } -    const inputs = $(selector); - -    inputs.each((i, el) => { -      const $input = $(el); - -      const calendar = new Pikaday({ -        field: $input.get(0), -        theme: 'gitlab-theme animate-picker', -        format: 'yyyy-mm-dd', -        minDate: new Date(), -        container: $input.parent().get(0), -        onSelect(dateText) { -          $input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); - -          $input.trigger('change'); - -          toggleClearInput.call($input); -        }, -      }); - -      calendar.setDate(new Date($input.val())); -      $input.data('pikaday', calendar); +import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; + +// Add datepickers to all `js-access-expiration-date` elements. If those elements are +// children of an element with the `clearable-input` class, and have a sibling +// `js-clear-input` element, then show that element when there is a value in the +// datepicker, and make clicking on that element clear the field. +// +export default function memberExpirationDate(selector = '.js-access-expiration-date') { +  function toggleClearInput() { +    $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); +  } +  const inputs = $(selector); + +  inputs.each((i, el) => { +    const $input = $(el); + +    const calendar = new Pikaday({ +      field: $input.get(0), +      theme: 'gitlab-theme animate-picker', +      format: 'yyyy-mm-dd', +      minDate: new Date(), +      container: $input.parent().get(0), +      parse: dateString => parsePikadayDate(dateString), +      toString: date => pikadayToString(date), +      onSelect(dateText) { +        $input.val(calendar.toString(dateText)); + +        $input.trigger('change'); + +        toggleClearInput.call($input); +      },      }); -    inputs.next('.js-clear-input').on('click', function clicked(event) { -      event.preventDefault(); +    calendar.setDate(parsePikadayDate($input.val())); +    $input.data('pikaday', calendar); +  }); -      const input = $(this).closest('.clearable-input').find(selector); -      const calendar = input.data('pikaday'); +  inputs.next('.js-clear-input').on('click', function clicked(event) { +    event.preventDefault(); -      calendar.setDate(null); -      input.trigger('change'); -      toggleClearInput.call(input); -    }); +    const input = $(this).closest('.clearable-input').find(selector); +    const calendar = input.data('pikaday'); + +    calendar.setDate(null); +    input.trigger('change'); +    toggleClearInput.call(input); +  }); -    inputs.on('blur', toggleClearInput); +  inputs.on('blur', toggleClearInput); -    inputs.each(toggleClearInput); -  }; -}).call(window); +  inputs.each(toggleClearInput); +} diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index 8291b8c4a70..6264750a4fb 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -1,81 +1,74 @@ -/* eslint-disable class-methods-use-this */ -(() => { -  window.gl = window.gl || {}; - -  class Members { -    constructor() { -      this.addListeners(); -      this.initGLDropdown(); -    } +export default class Members { +  constructor() { +    this.addListeners(); +    this.initGLDropdown(); +  } -    addListeners() { -      $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); -      $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); -      $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); -      gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); -    } +  addListeners() { +    $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); +    $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); +    $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); +    gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); +  } -    initGLDropdown() { -      $('.js-member-permissions-dropdown').each((i, btn) => { -        const $btn = $(btn); +  initGLDropdown() { +    $('.js-member-permissions-dropdown').each((i, btn) => { +      const $btn = $(btn); -        $btn.glDropdown({ -          selectable: true, -          isSelectable(selected, $el) { -            return !$el.hasClass('is-active'); -          }, -          fieldName: $btn.data('field-name'), -          id(selected, $el) { -            return $el.data('id'); -          }, -          toggleLabel(selected, $el) { -            return $el.text(); -          }, -          clicked: (options) => { -            this.formSubmit(null, options.$el); -          }, -        }); +      $btn.glDropdown({ +        selectable: true, +        isSelectable(selected, $el) { +          return !$el.hasClass('is-active'); +        }, +        fieldName: $btn.data('field-name'), +        id(selected, $el) { +          return $el.data('id'); +        }, +        toggleLabel(selected, $el) { +          return $el.text(); +        }, +        clicked: (options) => { +          this.formSubmit(null, options.$el); +        },        }); -    } - -    removeRow(e) { -      const $target = $(e.target); +    }); +  } +  // eslint-disable-next-line class-methods-use-this +  removeRow(e) { +    const $target = $(e.target); -      if ($target.hasClass('btn-remove')) { -        $target.closest('.member') -          .fadeOut(function fadeOutMemberRow() { -            $(this).remove(); -          }); -      } +    if ($target.hasClass('btn-remove')) { +      $target.closest('.member') +        .fadeOut(function fadeOutMemberRow() { +          $(this).remove(); +        });      } +  } -    formSubmit(e, $el = null) { -      const $this = e ? $(e.currentTarget) : $el; -      const { $toggle, $dateInput } = this.getMemberListItems($this); - -      $this.closest('form').trigger('submit.rails'); - -      $toggle.disable(); -      $dateInput.disable(); -    } +  formSubmit(e, $el = null) { +    const $this = e ? $(e.currentTarget) : $el; +    const { $toggle, $dateInput } = this.getMemberListItems($this); -    formSuccess(e) { -      const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member')); +    $this.closest('form').trigger('submit.rails'); -      $toggle.enable(); -      $dateInput.enable(); -    } +    $toggle.disable(); +    $dateInput.disable(); +  } -    getMemberListItems($el) { -      const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`); +  formSuccess(e) { +    const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member')); -      return { -        $memberListItem, -        $toggle: $memberListItem.find('.dropdown-menu-toggle'), -        $dateInput: $memberListItem.find('.js-access-expiration-date'), -      }; -    } +    $toggle.enable(); +    $dateInput.enable();    } +  // eslint-disable-next-line class-methods-use-this +  getMemberListItems($el) { +    const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`); -  gl.Members = Members; -})(); +    return { +      $memberListItem, +      $toggle: $memberListItem.find('.dropdown-menu-toggle'), +      $dateInput: $memberListItem.find('.js-access-expiration-date'), +    }; +  } +} diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js index 645045fea88..93f8f6ee926 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js @@ -1,8 +1,8 @@  /* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */  /* global ace */ -/* global Flash */  import Vue from 'vue'; +import Flash from '../../flash';  ((global) => {    global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index d74cf5328ad..17591829b76 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -1,7 +1,7 @@  /* eslint-disable new-cap, comma-dangle, no-new */ -/* global Flash */  import Vue from 'vue'; +import Flash from '../flash';  import initIssuableSidebar from '../init_issuable_sidebar';  import './merge_conflict_store';  import './merge_conflict_service'; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index c042b22d1fd..54c1b7a268e 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,9 +1,8 @@  /* eslint-disable no-new, class-methods-use-this */ -/* global Flash */  /* global notes */  import Cookies from 'js-cookie'; -import './flash'; +import Flash from './flash';  import BlobForkSuggestion from './blob/blob_fork_suggestion';  import initChangesDropdown from './init_changes_dropdown';  import bp from './breakpoints'; @@ -12,8 +11,8 @@ import {    handleLocationHash,    isMetaClick,  } from './lib/utils/common_utils'; -  import initDiscussionTab from './image_diff/init_discussion_tab'; +import Diff from './diff';  /* eslint-disable max-len */  // MergeRequestTabs @@ -68,6 +67,10 @@ import initDiscussionTab from './image_diff/init_discussion_tab';    class MergeRequestTabs {      constructor({ action, setUrl, stubLocation } = {}) { +      const mergeRequestTabs = document.querySelector('.js-tabs-affix'); +      const navbar = document.querySelector('.navbar-gitlab'); +      const paddingTop = 16; +        this.diffsLoaded = false;        this.pipelinesLoaded = false;        this.commitsLoaded = false; @@ -77,6 +80,11 @@ import initDiscussionTab from './image_diff/init_discussion_tab';        this.setCurrentAction = this.setCurrentAction.bind(this);        this.tabShown = this.tabShown.bind(this);        this.showTab = this.showTab.bind(this); +      this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0; + +      if (mergeRequestTabs) { +        this.stickyTop += mergeRequestTabs.offsetHeight; +      }        if (stubLocation) {          location = stubLocation; @@ -279,7 +287,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';            const $container = $('#diffs');            $container.html(data.html); -          initChangesDropdown(); +          initChangesDropdown(this.stickyTop);            if (typeof gl.diffNotesCompileComponents !== 'undefined') {              gl.diffNotesCompileComponents(); @@ -293,7 +301,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';            }            this.diffsLoaded = true; -          new gl.Diff(); +          new Diff();            this.scrollToElement('#diffs');            $('.diff-file').each((i, el) => { diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 3e07ec4d0aa..8f3f1986763 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,7 +1,8 @@  /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */ -/* global Flash */  /* global Sortable */ +import Flash from './flash'; +  (function() {    this.Milestone = (function() {      function Milestone() { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 4675b1fcb8f..74e5a4f1cea 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -5,7 +5,7 @@ import _ from 'underscore';  (function() {    this.MilestoneSelect = (function() { -    function MilestoneSelect(currentProject, els) { +    function MilestoneSelect(currentProject, els, options = {}) {        var _this, $els;        if (currentProject != null) {          _this = this; @@ -136,18 +136,27 @@ import _ from 'underscore';            },            opened: function(e) {              const $el = $(e.currentTarget); -            if ($dropdown.hasClass('js-issue-board-sidebar')) { +            if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {                selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;              }              $('a.is-active', $el).removeClass('is-active');              $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');            },            vue: $dropdown.hasClass('js-issue-board-sidebar'), -          clicked: function(options) { -            const { $el, e } = options; -            let selected = options.selectedObj; +          clicked: function(clickEvent) { +            const { $el, e } = clickEvent; +            let selected = clickEvent.selectedObj; +              var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; -            page = $('body').data('page'); +            if (!selected) return; + +            if (options.handleClick) { +              e.preventDefault(); +              options.handleClick(selected); +              return; +            } + +            page = $('body').attr('data-page');              isIssueIndex = page === 'projects:issues:index';              isMRIndex = (page === page && page === 'projects:merge_requests:index');              isSelecting = (selected.name !== selectedMilestone); diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index 64c1447f427..ca3d271663b 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -1,5 +1,5 @@  /* eslint-disable no-new */ -/* global Flash */ +import Flash from './flash';  /**   * In each pipelines table we have a mini pipeline graph for each pipeline. diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 442ed86d50c..cbe24c0915b 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -1,6 +1,6 @@  <script> -  /* global Flash */    import _ from 'underscore'; +  import Flash from '../../flash';    import MonitoringService from '../services/monitoring_service';    import GraphGroup from './graph_group.vue';    import Graph from './graph.vue'; diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 5aa3865f96a..f8782fde927 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -138,7 +138,7 @@        renderAxesPaths() {          this.timeSeries = createTimeSeries( -          this.graphData.queries[0], +          this.graphData.queries,            this.graphWidth,            this.graphHeight,            this.graphHeightOffset, @@ -153,8 +153,9 @@          const axisYScale = d3.scale.linear()            .range([this.graphHeight - this.graphHeightOffset, 0]); -        axisXScale.domain(d3.extent(this.timeSeries[0].values, d => d.time)); -        axisYScale.domain([0, d3.max(this.timeSeries[0].values.map(d => d.value))]); +        const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []); +        axisXScale.domain(d3.extent(allValues, d => d.time)); +        axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);          const xAxis = d3.svg.axis()            .scale(axisXScale) @@ -246,6 +247,7 @@                :key="index"                :generated-line-path="path.linePath"                :generated-area-path="path.areaPath" +              :line-style="path.lineStyle"                :line-color="path.lineColor"                :area-color="path.areaColor"              /> diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index 85b6d7f4cbe..440b1b12631 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -79,7 +79,8 @@        },        formatMetricUsage(series) { -        const value = series.values[this.currentDataIndex].value; +        const value = series.values[this.currentDataIndex] && +          series.values[this.currentDataIndex].value;          if (isNaN(value)) {            return '-';          } @@ -92,6 +93,12 @@          }          return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;        }, + +      strokeDashArray(type) { +        if (type === 'dashed') return '6, 3'; +        if (type === 'dotted') return '3, 3'; +        return null; +      },      },      mounted() {        this.$nextTick(() => { @@ -162,13 +169,15 @@        v-for="(series, index) in timeSeries"        :key="index"        :transform="translateLegendGroup(index)"> -      <rect -        :fill="series.areaColor" -        :width="measurements.legends.width" -        :height="measurements.legends.height" -        x="20" -        :y="graphHeight - measurements.legendOffset"> -      </rect> +      <line +        :stroke="series.lineColor" +        :stroke-width="measurements.legends.height" +        :stroke-dasharray="strokeDashArray(series.lineStyle)" +        :x1="measurements.legends.offsetX" +        :x2="measurements.legends.offsetX + measurements.legends.width" +        :y1="graphHeight - measurements.legends.offsetY" +        :y2="graphHeight - measurements.legends.offsetY"> +      </line>        <text          v-if="timeSeries.length > 1"          class="legend-metric-title" diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue index 043f1bf66bb..5e6d409033a 100644 --- a/app/assets/javascripts/monitoring/components/graph/path.vue +++ b/app/assets/javascripts/monitoring/components/graph/path.vue @@ -9,6 +9,10 @@          type: String,          required: true,        }, +      lineStyle: { +        type: String, +        required: false, +      },        lineColor: {          type: String,          required: true, @@ -18,6 +22,13 @@          required: true,        },      }, +    computed: { +      strokeDashArray() { +        if (this.lineStyle === 'dashed') return '3, 1'; +        if (this.lineStyle === 'dotted') return '1, 1'; +        return null; +      }, +    },    };  </script>  <template> @@ -34,6 +45,7 @@        :stroke="lineColor"        fill="none"        stroke-width="1" +      :stroke-dasharray="strokeDashArray"        transform="translate(-5, 20)">      </path>    </g> diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js index ee3c45efacc..ee866850e13 100644 --- a/app/assets/javascripts/monitoring/utils/measurements.js +++ b/app/assets/javascripts/monitoring/utils/measurements.js @@ -7,15 +7,16 @@ export default {        left: 40,      },      legends: { -      width: 10, +      width: 15,        height: 3, +      offsetX: 20, +      offsetY: 32,      },      backgroundLegend: {        width: 30,        height: 50,      },      axisLabelLineOffset: -20, -    legendOffset: 33,    },    large: { // This covers both md and lg screen sizes      margin: { @@ -27,13 +28,14 @@ export default {      legends: {        width: 15,        height: 3, +      offsetX: 20, +      offsetY: 34,      },      backgroundLegend: {        width: 30,        height: 150,      },      axisLabelLineOffset: 20, -    legendOffset: 36,    },    xTicks: 8,    yTicks: 3, diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js index 65eec0d8d02..d21a265bd43 100644 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -11,7 +11,9 @@ const defaultColorPalette = {  const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple']; -export default function createTimeSeries(queryData, graphWidth, graphHeight, graphHeightOffset) { +const defaultStyleOrder = ['solid', 'dashed', 'dotted']; + +function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {    let usedColors = [];    function pickColor(name) { @@ -31,17 +33,7 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra      return defaultColorPalette[pick];    } -  const maxValues = queryData.result.map((timeSeries, index) => { -    const maxValue = d3.max(timeSeries.values.map(d => d.value)); -    return { -      maxValue, -      index, -    }; -  }); - -  const maxValueFromSeries = _.max(maxValues, val => val.maxValue); - -  return queryData.result.map((timeSeries, timeSeriesNumber) => { +  return query.result.map((timeSeries, timeSeriesNumber) => {      let metricTag = '';      let lineColor = '';      let areaColor = ''; @@ -52,9 +44,9 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra      const timeSeriesScaleY = d3.scale.linear()        .range([graphHeight - graphHeightOffset, 0]); -    timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time)); +    timeSeriesScaleX.domain(xDom);      timeSeriesScaleX.ticks(d3.time.minute, 60); -    timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]); +    timeSeriesScaleY.domain(yDom);      const defined = d => !isNaN(d.value) && d.value != null; @@ -72,10 +64,10 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra        .y1(d => timeSeriesScaleY(d.value));      const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]]; -    const seriesCustomizationData = queryData.series != null && -                                    _.findWhere(queryData.series[0].when, -                                    { value: timeSeriesMetricLabel }); -    if (seriesCustomizationData != null) { +    const seriesCustomizationData = query.series != null && +      _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel }); + +    if (seriesCustomizationData) {        metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;        [lineColor, areaColor] = pickColor(seriesCustomizationData.color);      } else { @@ -83,14 +75,35 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra        [lineColor, areaColor] = pickColor();      } +    if (query.track) { +      metricTag += ` - ${query.track}`; +    } +      return {        linePath: lineFunction(timeSeries.values),        areaPath: areaFunction(timeSeries.values),        timeSeriesScaleX,        values: timeSeries.values, +      lineStyle,        lineColor,        areaColor,        metricTag,      };    });  } + +export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) { +  const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat( +    query.result.reduce((allResults, result) => allResults.concat(result.values), []), +  ), []); + +  const xDom = d3.extent(allValues, d => d.time); +  const yDom = [0, d3.max(allValues.map(d => d.value))]; + +  return queries.reduce((series, query, index) => { +    const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length]; +    return series.concat( +      queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle), +    ); +  }, []); +} diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 5da2db063a4..1d496c64e53 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,85 +1,57 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */  import Api from './api'; +import './lib/utils/url_utility'; -(function() { -  window.NamespaceSelect = (function() { -    function NamespaceSelect(opts) { -      this.onSelectItem = this.onSelectItem.bind(this); -      var fieldName, showAny; -      this.dropdown = opts.dropdown; -      showAny = true; -      fieldName = 'namespace_id'; -      if (this.dropdown.attr('data-field-name')) { -        fieldName = this.dropdown.data('fieldName'); -      } -      if (this.dropdown.attr('data-show-any')) { -        showAny = this.dropdown.data('showAny'); -      } -      this.dropdown.glDropdown({ -        filterable: true, -        selectable: true, -        filterRemote: true, -        search: { -          fields: ['path'] -        }, -        fieldName: fieldName, -        toggleLabel: function(selected) { -          if (selected.id == null) { -            return selected.text; -          } else { -            return selected.kind + ": " + selected.full_path; -          } -        }, -        data: function(term, dataCallback) { -          return Api.namespaces(term, function(namespaces) { -            var anyNamespace; -            if (showAny) { -              anyNamespace = { -                text: 'Any namespace', -                id: null -              }; -              namespaces.unshift(anyNamespace); -              namespaces.splice(1, 0, 'divider'); -            } -            return dataCallback(namespaces); -          }); -        }, -        text: function(namespace) { -          if (namespace.id == null) { -            return namespace.text; -          } else { -            return namespace.kind + ": " + namespace.full_path; -          } -        }, -        renderRow: this.renderRow, -        clicked: this.onSelectItem -      }); -    } - -    NamespaceSelect.prototype.onSelectItem = function(options) { -      const { e } = options; -      return e.preventDefault(); -    }; +export default class NamespaceSelect { +  constructor(opts) { +    const isFilter = opts.dropdown.dataset.isFilter === 'true'; +    const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id'; -    return NamespaceSelect; -  })(); - -  window.NamespaceSelects = (function() { -    function NamespaceSelects(opts) { -      var ref; -      if (opts == null) { -        opts = {}; -      } -      this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-namespace-select'); -      this.$dropdowns.each(function(i, dropdown) { -        var $dropdown; -        $dropdown = $(dropdown); -        return new window.NamespaceSelect({ -          dropdown: $dropdown +    $(opts.dropdown).glDropdown({ +      filterable: true, +      selectable: true, +      filterRemote: true, +      search: { +        fields: ['path'] +      }, +      fieldName: fieldName, +      toggleLabel: function(selected) { +        if (selected.id == null) { +          return selected.text; +        } else { +          return selected.kind + ": " + selected.full_path; +        } +      }, +      data: function(term, dataCallback) { +        return Api.namespaces(term, function(namespaces) { +          if (isFilter) { +            const anyNamespace = { +              text: 'Any namespace', +              id: null +            }; +            namespaces.unshift(anyNamespace); +            namespaces.splice(1, 0, 'divider'); +          } +          return dataCallback(namespaces);          }); -      }); -    } - -    return NamespaceSelects; -  })(); -}).call(window); +      }, +      text: function(namespace) { +        if (namespace.id == null) { +          return namespace.text; +        } else { +          return namespace.kind + ": " + namespace.full_path; +        } +      }, +      renderRow: this.renderRow, +      clicked(options) { +        if (!isFilter) { +          const { e } = options; +          e.preventDefault(); +        } +      }, +      url(namespace) { +        return gl.utils.mergeUrlParams({ [fieldName]: namespace.id }, window.location.href); +      }, +    }); +  } +} diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index 8aae2ad201c..129f1724cb8 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,6 +1,6 @@  /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */ -/* global ShortcutsNetwork */ +import ShortcutsNetwork from '../shortcuts_network';  import Network from './network';  $(function() { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 24de21f2ce2..e1ab28978e8 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -5,29 +5,27 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign,  no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new,  brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow,  newline-per-chained-call, no-useless-escape, class-methods-use-this */ -/* global Flash */ -/* global Autosave */ +  /* global ResolveService */  /* global mrRefreshWidgetUrl */  import $ from 'jquery';  import _ from 'underscore';  import Cookies from 'js-cookie'; -import autosize from 'vendor/autosize'; -import Dropzone from 'dropzone'; +import Autosize from 'autosize';  import 'vendor/jquery.caret'; // required by jquery.atwho  import 'vendor/jquery.atwho';  import AjaxCache from '~/lib/utils/ajax_cache'; +import Flash from './flash';  import CommentTypeToggle from './comment_type_toggle'; +import GLForm from './gl_form';  import loadAwardsHandler from './awards_handler'; -import './autosave'; -import './dropzone_input'; +import Autosave from './autosave';  import TaskList from './task_list';  import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';  import imageDiffHelper from './image_diff/helpers/index'; -window.autosize = autosize; -window.Dropzone = Dropzone; +window.autosize = Autosize;  function normalizeNewlines(str) {    return str.replace(/\r\n/g, '\n'); @@ -354,7 +352,7 @@ export default class Notes {              Object.keys(noteEntity.commands_changes).length > 0) {            $notesList.find('.system-note.being-posted').remove();          } -        this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); +        this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0));          this.refresh();        }        return; @@ -415,8 +413,9 @@ export default class Notes {        return;      }      this.note_ids.push(noteEntity.id); +      form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); -    row = form.closest('tr'); +    row = (form.length || !noteEntity.discussion_line_code) ? form.closest('tr') : $(`#${noteEntity.discussion_line_code}`);      if (noteEntity.on_image) {        row = form; @@ -557,7 +556,7 @@ export default class Notes {     */    setupNoteForm(form) {      var textarea, key; -    new gl.GLForm(form, this.enableGFM); +    this.glForm = new GLForm(form, this.enableGFM);      textarea = form.find('.js-note-text');      key = [        'Note', @@ -593,7 +592,7 @@ export default class Notes {      } else if ($form.hasClass('js-discussion-note-form')) {        formParentTimeline = $form.closest('.discussion-notes').find('.notes');      } -    return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline); +    return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline.get(0));    }    updateNoteError($parentTimeline) { @@ -1152,7 +1151,7 @@ export default class Notes {      var targetId = $originalContentEl.data('target-id');      var targetType = $originalContentEl.data('target-type'); -    new gl.GLForm($editForm.find('form'), this.enableGFM); +    this.glForm = new GLForm($editForm.find('form'), this.enableGFM);      $editForm.find('form')        .attr('action', postUrl) @@ -1213,13 +1212,13 @@ export default class Notes {    }    addFlash(...flashParams) { -    this.flashInstance = new Flash(...flashParams); +    this.flashContainer = new Flash(...flashParams);    }    clearFlash() { -    if (this.flashInstance && this.flashInstance.flashContainer) { -      this.flashInstance.flashContainer.hide(); -      this.flashInstance = null; +    if (this.flashContainer) { +      this.flashContainer.style.display = 'none'; +      this.flashContainer = null;      }    } @@ -1257,7 +1256,7 @@ export default class Notes {    }    static checkMergeRequestStatus() { -    if (getPagePath(1) === 'merge_requests') { +    if (getPagePath(1) === 'merge_requests' && gl.mrWidget) {        gl.mrWidget.checkStatus();      }    } @@ -1282,10 +1281,12 @@ export default class Notes {     * Get data from Form attributes to use for saving/submitting comment.     */    getFormData($form) { +    const content = $form.find('.js-note-text').val();      return {        formData: $form.serialize(), -      formContent: _.escape($form.find('.js-note-text').val()), +      formContent: _.escape(content),        formAction: $form.attr('action'), +      formContentOriginal: content,      };    } @@ -1417,7 +1418,7 @@ export default class Notes {      const isMainForm = $form.hasClass('js-main-target-form');      const isDiscussionForm = $form.hasClass('js-discussion-note-form');      const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); -    const { formData, formContent, formAction } = this.getFormData($form); +    const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);      let noteUniqueId;      let systemNoteUniqueId;      let hasQuickActions = false; @@ -1576,7 +1577,7 @@ export default class Notes {            $form = $notesContainer.parent().find('form');          } -        $form.find('.js-note-text').val(formContent); +        $form.find('.js-note-text').val(formContentOriginal);          this.reenableTargetFormSubmitButton(e);          this.addNoteError($form);        }); diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index ab8516296a8..db8f85759b2 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,9 +1,9 @@  <script> -  /* global Flash, Autosave */    import { mapActions, mapGetters } from 'vuex';    import _ from 'underscore'; -  import autosize from 'vendor/autosize'; -  import '../../autosave'; +  import Autosize from 'autosize'; +  import Flash from '../../flash'; +  import Autosave from '../../autosave';    import TaskList from '../../task_list';    import * as constants from '../constants';    import eventHub from '../event_hub'; @@ -145,7 +145,7 @@                    Flash(                      'Something went wrong while adding your comment. Please try again.',                      'alert', -                    $(this.$refs.commentForm), +                    this.$refs.commentForm,                    );                  }                } else { @@ -160,7 +160,7 @@                this.isSubmitting = false;                this.discard(false);                const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; -              Flash(msg, 'alert', $(this.$el)); +              Flash(msg, 'alert', this.$el);                this.note = noteData.data.note.note; // Restore textarea content.                this.removePlaceholderNotes();              }); @@ -219,7 +219,7 @@        },        resizeTextarea() {          this.$nextTick(() => { -          autosize.update(this.$refs.textarea); +          Autosize.update(this.$refs.textarea);          });        },      }, diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index b131ef4b182..0f13221b81e 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -1,6 +1,6 @@  <script> -  /* global Flash */    import { mapActions, mapGetters } from 'vuex'; +  import Flash from '../../flash';    import { SYSTEM_NOTE } from '../constants';    import issueNote from './issue_note.vue';    import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -9,8 +9,8 @@    import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';    import issueNoteEditedText from './issue_note_edited_text.vue';    import issueNoteForm from './issue_note_form.vue'; -  import placeholderNote from './issue_placeholder_note.vue'; -  import placeholderSystemNote from './issue_placeholder_system_note.vue'; +  import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; +  import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';    import autosave from '../mixins/autosave';    export default { @@ -133,7 +133,7 @@              this.isReplying = true;              this.$nextTick(() => {                const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; -              Flash(msg, 'alert', $(this.$el)); +              Flash(msg, 'alert', this.$el);                this.$refs.noteForm.note = noteText;                callback(err);              }); diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 1f43b8a16ad..40318f9a600 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -1,7 +1,6 @@  <script> -  /* global Flash */ -    import { mapGetters, mapActions } from 'vuex'; +  import Flash from '../../flash';    import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';    import issueNoteHeader from './issue_note_header.vue';    import issueNoteActions from './issue_note_actions.vue'; @@ -101,7 +100,7 @@              this.isEditing = true;              this.$nextTick(() => {                const msg = 'Something went wrong while editing your comment. Please try again.'; -              Flash(msg, 'alert', $(this.$el)); +              Flash(msg, 'alert', this.$el);                this.recoverNoteContent(noteText);                callback();              }); @@ -123,7 +122,9 @@          // we need to do this to prevent noteForm inconsistent content warning          // this is something we intentionally do so we need to recover the content          this.note.note = noteText; -        this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better +        if (this.$refs.noteBody) { +          this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better +        }        },      },      created() { diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index d42e61e3899..c3a340139e7 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -1,10 +1,9 @@  <script> -  /* global Flash */ -    import { mapActions, mapGetters } from 'vuex';    import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';    import emojiSmile from 'icons/_emoji_smile.svg';    import emojiSmiley from 'icons/_emoji_smiley.svg'; +  import Flash from '../../flash';    import { glEmojiTag } from '../../emoji';    import tooltip from '../../vue_shared/directives/tooltip'; diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index b6fc5e5036f..5c9119644e3 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -1,14 +1,14 @@  <script> -  /* global Flash */    import { mapGetters, mapActions } from 'vuex'; +  import Flash from '../../flash';    import store from '../stores/';    import * as constants from '../constants';    import issueNote from './issue_note.vue';    import issueDiscussion from './issue_discussion.vue'; -  import issueSystemNote from './issue_system_note.vue'; +  import systemNote from '../../vue_shared/components/notes/system_note.vue';    import issueCommentForm from './issue_comment_form.vue'; -  import placeholderNote from './issue_placeholder_note.vue'; -  import placeholderSystemNote from './issue_placeholder_system_note.vue'; +  import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; +  import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';    import loadingIcon from '../../vue_shared/components/loading_icon.vue';    export default { @@ -37,7 +37,7 @@      components: {        issueNote,        issueDiscussion, -      issueSystemNote, +      systemNote,        issueCommentForm,        loadingIcon,        placeholderNote, @@ -68,7 +68,7 @@            }            return placeholderNote;          } else if (note.individual_note) { -          return note.notes[0].system ? issueSystemNote : issueNote; +          return note.notes[0].system ? systemNote : issueNote;          }          return issueDiscussion; diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js index 5843b97f225..a008171beda 100644 --- a/app/assets/javascripts/notes/mixins/autosave.js +++ b/app/assets/javascripts/notes/mixins/autosave.js @@ -1,5 +1,4 @@ -/* globals Autosave */ -import '../../autosave'; +import Autosave from '../../autosave';  export default {    methods: { diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 1a791039909..6f04aecc9b7 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -1,5 +1,5 @@ -/* global Flash */  import Visibility from 'visibilityjs'; +import Flash from '../../flash';  import Poll from '../../lib/utils/poll';  import * as types from './mutation_types';  import * as utils from './utils'; @@ -99,7 +99,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {          eTagPoll.makeRequest();          $('.js-gfm-input').trigger('clear-commands-cache.atwho'); -        Flash('Commands applied', 'notice', $(noteData.flashContainer)); +        Flash('Commands applied', 'notice', noteData.flashContainer);        }        if (commandsChanges) { @@ -114,8 +114,8 @@ export const saveNote = ({ commit, dispatch }, noteData) => {              .catch(() => {                Flash(                  'Something went wrong while adding your award. Please try again.', -                null, -                $(noteData.flashContainer), +                'alert', +                noteData.flashContainer,                );              });          } @@ -126,7 +126,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {        }        if (errors && errors.commands_only) { -        Flash(errors.commands_only, 'notice', $(noteData.flashContainer)); +        Flash(errors.commands_only, 'notice', noteData.flashContainer);        }        commit(types.REMOVE_PLACEHOLDER_NOTES); diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index 838356133cd..f90ac2d9f71 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -1,5 +1,5 @@  /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */ -/* global Flash */ +import Flash from './flash';  (function() {    this.NotificationsDropdown = (function() { diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js index 50c725aa3d5..f1cf6e92ef5 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js @@ -1,5 +1,6 @@  import Vue from 'vue';  import Translate from '../vue_shared/translate'; +import GlFieldErrors from '../gl_field_errors';  import intervalPatternInput from './components/interval_pattern_input.vue';  import TimezoneDropdown from './components/timezone_dropdown';  import TargetBranchDropdown from './components/target_branch_dropdown'; @@ -39,7 +40,7 @@ document.addEventListener('DOMContentLoaded', () => {    gl.timezoneDropdown = new TimezoneDropdown();    gl.targetBranchDropdown = new TargetBranchDropdown(); -  gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement); +  gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);    setupPipelineVariableList($('.js-pipeline-variable-list'));  }); diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 54227425d2a..547140b1a43 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,6 +1,6 @@  <script> -  import getActionIcon from '../../../vue_shared/ci_action_icons';    import tooltip from '../../../vue_shared/directives/tooltip'; +  import icon from '../../../vue_shared/components/icon.vue';    /**     * Renders either a cancel, retry or play icon pointing to the given path. @@ -29,17 +29,18 @@        },      }, +    components: { +      icon, +    }, +      directives: {        tooltip,      },      computed: { -      actionIconSvg() { -        return getActionIcon(this.actionIcon); -      }, -        cssClass() { -        return `js-${gl.text.dasherize(this.actionIcon)}`; +        const actionIconDash = gl.text.dasherize(this.actionIcon); +        return `${actionIconDash} js-icon-${actionIconDash}`;        },      },    }; @@ -50,14 +51,9 @@      :data-method="actionMethod"      :title="tooltipText"      :href="link" -    class="ci-action-icon-container" +    class="ci-action-icon-container ci-action-icon-wrapper" +    :class="cssClass"      data-container="body"> - -    <i -      class="ci-action-icon-wrapper" -      :class="cssClass" -      v-html="actionIconSvg" -      aria-hidden="true" -      /> +    <icon :name="actionIcon"/>    </a>  </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue index 18fe1847eef..1c0944d45fc 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue @@ -1,5 +1,5 @@  <script> -  import getActionIcon from '../../../vue_shared/ci_action_icons'; +  import icon from '../../../vue_shared/components/icon.vue';    import tooltip from '../../../vue_shared/directives/tooltip';    /** @@ -29,14 +29,12 @@        },      }, -    directives: { -      tooltip, +    components: { +      icon,      }, -    computed: { -      actionIconSvg() { -        return getActionIcon(this.actionIcon); -      }, +    directives: { +      tooltip,      },    };  </script> @@ -49,7 +47,7 @@      rel="nofollow"      class="ci-action-icon-wrapper js-ci-status-icon"      data-container="body" -    v-html="actionIconSvg"      aria-label="Job's action"> +    <icon :name="actionIcon"/>    </a>  </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index 3e5d6d15909..7006d05e7b2 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -18,7 +18,7 @@     *     "group": "success",     *     "details_path": "/root/ci-mock/builds/4256",     *     "action": { -   *       "icon": "icon_action_retry", +   *       "icon": "retry",     *       "title": "Retry",     *       "path": "/root/ci-mock/builds/4256/retry",     *       "method": "post" diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index 3933509a6f4..5dea4555515 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -19,7 +19,7 @@     *     "group": "success",     *     "details_path": "/root/ci-mock/builds/4256",     *     "action": { -   *       "icon": "icon_action_retry", +   *       "icon": "retry",     *       "title": "Retry",     *       "path": "/root/ci-mock/builds/4256/retry",     *       "method": "post" diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 085bd20cefe..3da60e88474 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -12,6 +12,15 @@          type: Object,          required: true,        }, +      // Can be rendered in 3 different places, with some visual differences +      // Accepts root | child +      // `root` -> main view +      // `child` -> rendered inside MR or Commit View +      viewType: { +        type: String, +        required: false, +        default: 'root', +      },      },      components: {        tablePagination, @@ -187,7 +196,7 @@          :empty-state-svg-path="emptyStateSvgPath"          /> -      <error-state  +      <error-state          v-if="shouldRenderErrorState"          :error-state-svg-path="errorStateSvgPath"          /> @@ -206,6 +215,7 @@            :pipelines="state.pipelines"            :update-graph-dropdown="updateGraphDropdown"            :auto-devops-help-path="autoDevopsPath" +          :view-type="viewType"            />        </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index c4c63a52358..f3c0aca17ba 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -1,6 +1,4 @@  <script> -  /* global Flash */ -  import '~/flash';    import playIconSvg from 'icons/_icon_play.svg';    import eventHub from '../event_hub';    import loadingIcon from '../../vue_shared/components/loading_icon.vue'; diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 7aa0c0e8a7f..16a705cbaff 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -21,6 +21,10 @@          type: String,          required: true,        }, +      viewType: { +        type: String, +        required: true, +      },      },      components: {        pipelinesTableRowComponent, @@ -59,6 +63,7 @@        :pipeline="model"        :update-graph-dropdown="updateGraphDropdown"        :auto-devops-help-path="autoDevopsHelpPath" +      :view-type="viewType"      />    </div>  </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 5b9bb6c3750..33fbce993b2 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -29,6 +29,10 @@ export default {        type: String,        required: true,      }, +    viewType: { +      type: String, +      required: true, +    },    },    components: {      asyncButtonComponent, @@ -203,9 +207,13 @@ export default {      displayPipelineActions() {        return this.pipeline.flags.retryable || -             this.pipeline.flags.cancelable || -             this.pipeline.details.manual_actions.length || -             this.pipeline.details.artifacts.length; +        this.pipeline.flags.cancelable || +        this.pipeline.details.manual_actions.length || +        this.pipeline.details.artifacts.length; +    }, + +    isChildView() { +      return this.viewType === 'child';      },    },  }; @@ -218,7 +226,10 @@ export default {          Status        </div>        <div class="table-mobile-content"> -        <ci-badge :status="pipelineStatus"/> +        <ci-badge +          :status="pipelineStatus" +          :show-text="!isChildView" +          />        </div>      </div> @@ -240,7 +251,9 @@ export default {            :commit-url="commitUrl"            :short-sha="commitShortSha"            :title="commitTitle" -          :author="commitAuthor"/> +          :author="commitAuthor" +          :show-branch="!isChildView" +          />        </div>      </div> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index a4a27247406..ac9d9c901ca 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -13,8 +13,8 @@   * 4. Commit widget   */ -/* global Flash */ -import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons'; +import Flash from '../../flash'; +import icon from '../../vue_shared/components/icon.vue';  import loadingIcon from '../../vue_shared/components/loading_icon.vue';  import tooltip from '../../vue_shared/directives/tooltip'; @@ -45,6 +45,7 @@ export default {    components: {      loadingIcon, +    icon,    },    updated() { @@ -122,8 +123,8 @@ export default {        return `ci-status-icon-${this.stage.status.group}`;      }, -    svgIcon() { -      return borderlessStatusIconEntityMap[this.stage.status.icon]; +    borderlessIcon() { +      return `${this.stage.status.icon}_borderless`;      },    },  }; @@ -145,9 +146,10 @@ export default {        aria-expanded="false">        <span -        v-html="svgIcon"          aria-hidden="true"          :aria-label="stage.title"> +        <icon +          :name="borderlessIcon"/>        </span>        <i diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index e97f5632dc8..50bdf80c3e3 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -1,6 +1,5 @@ -/* global Flash */ -import '~/flash';  import Visibility from 'visibilityjs'; +import Flash from '../../flash';  import Poll from '../../lib/utils/poll';  import emptyState from '../components/empty_state.vue';  import errorState from '../components/error_state.vue'; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index bfc416da50b..206023d4ddb 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -1,6 +1,5 @@ -/* global Flash */ -  import Vue from 'vue'; +import Flash from '../flash';  import PipelinesMediator from './pipeline_details_mediatior';  import pipelineGraph from './components/graph/graph_component.vue';  import pipelineHeader from './components/header_component.vue'; diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js index 385e7430a7d..823ccd849f4 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js +++ b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js @@ -1,6 +1,5 @@ -/* global Flash */ -  import Visibility from 'visibilityjs'; +import Flash from '../flash';  import Poll from '../lib/utils/poll';  import PipelineStore from './stores/pipeline_store';  import PipelineService from './services/pipeline_service'; diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index b2b34cb83e1..6348a2e331d 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -98,7 +98,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),        @toggle="toggleOpen"        @submit="onSubmit"> -      <template slot="body" scope="props"> +      <template slot="body" slot-scope="props">          <p v-html="props.text"></p>          <form diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index 3deb242bc1f..0dc02f012e4 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -1,5 +1,5 @@  /* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */ -/* global Flash */ +import Flash from '../flash';  import { getPagePath } from '../lib/utils/common_utils';  ((global) => { diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 11f9754780d..19682b20a4a 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -1,5 +1,6 @@  /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */ -/* global fuzzaldrinPlus */ + +import fuzzaldrinPlus from 'fuzzaldrin-plus';  (function() {    this.ProjectFindFile = (function() { diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index fb01390f91c..bffc85e6315 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -2,13 +2,15 @@  import Api from './api';  import ProjectSelectComboButton from './project_select_combo_button'; -(function() { -  this.ProjectSelect = (function() { +(function () { +  this.ProjectSelect = (function () {      function ProjectSelect() {        $('.ajax-project-select').each(function(i, select) {          var placeholder; +        const simpleFilter = $(select).data('simple-filter') || false;          this.groupId = $(select).data('group-id');          this.includeGroups = $(select).data('include-groups'); +        this.allProjects = $(select).data('all-projects') || false;          this.orderBy = $(select).data('order-by') || 'id';          this.withIssuesEnabled = $(select).data('with-issues-enabled');          this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled'); @@ -21,10 +23,10 @@ import ProjectSelectComboButton from './project_select_combo_button';          $(select).select2({            placeholder: placeholder,            minimumInputLength: 0, -          query: (function(_this) { -            return function(query) { +          query: (function (_this) { +            return function (query) {                var finalCallback, projectsCallback; -              finalCallback = function(projects) { +              finalCallback = function (projects) {                  var data;                  data = {                    results: projects @@ -32,9 +34,9 @@ import ProjectSelectComboButton from './project_select_combo_button';                  return query.callback(data);                };                if (_this.includeGroups) { -                projectsCallback = function(projects) { +                projectsCallback = function (projects) {                    var groupsCallback; -                  groupsCallback = function(groups) { +                  groupsCallback = function (groups) {                      var data;                      data = groups.concat(projects);                      return finalCallback(data); @@ -50,23 +52,25 @@ import ProjectSelectComboButton from './project_select_combo_button';                  return Api.projects(query.term, {                    order_by: _this.orderBy,                    with_issues_enabled: _this.withIssuesEnabled, -                  with_merge_requests_enabled: _this.withMergeRequestsEnabled +                  with_merge_requests_enabled: _this.withMergeRequestsEnabled, +                  membership: !_this.allProjects,                  }, projectsCallback);                }              };            })(this),            id: function(project) { +            if (simpleFilter) return project.id;              return JSON.stringify({                name: project.name,                url: project.web_url,              });            }, -          text: function(project) { +          text: function (project) {              return project.name_with_namespace || project.name;            },            dropdownCssClass: "ajax-project-dropdown"          }); - +        if (simpleFilter) return select;          return new ProjectSelectComboButton(select);        });      } diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 7f972b6f6ee..3ecc0c2a6e5 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -29,6 +29,12 @@ const bindEvents = () => {    const $newProjectForm = $('#new_project');    const $projectImportUrl = $('#project_import_url');    const $projectPath = $('#project_path'); +  const $useTemplateBtn = $('.template-button > input'); +  const $projectFieldsForm = $('.project-fields-form'); +  const $selectedTemplateText = $('.selected-template'); +  const $changeTemplateBtn = $('.change-template'); +  const $selectedIcon = $('.selected-icon svg'); +  const $templateProjectNameInput = $('#template-project-name #project_path');    if ($newProjectForm.length !== 1) {      return; @@ -48,6 +54,40 @@ const bindEvents = () => {      $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`);    }); +  function chooseTemplate() { +    $('.template-option').hide(); +    $projectFieldsForm.addClass('selected'); +    $selectedIcon.removeClass('active'); +    const value = $(this).val(); +    const templates = { +      rails: { +        text: 'Ruby on Rails', +        icon: '.selected-icon .icon-rails', +      }, +      express: { +        text: 'NodeJS Express', +        icon: '.selected-icon .icon-node-express', +      }, +      spring: { +        text: 'Spring', +        icon: '.selected-icon .icon-java-spring', +      }, +    }; + +    const selectedTemplate = templates[value]; +    $selectedTemplateText.text(selectedTemplate.text); +    $(selectedTemplate.icon).addClass('active'); +    $templateProjectNameInput.focus(); +  } + +  $useTemplateBtn.on('change', chooseTemplate); + +  $changeTemplateBtn.on('click', () => { +    $('.template-option').show(); +    $projectFieldsForm.removeClass('selected'); +    $useTemplateBtn.prop('checked', false); +  }); +    $newProjectForm.on('submit', () => {      $projectPath.val($projectPath.val().trim());    }); diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index a4d50a52315..55c93923cc8 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -81,7 +81,11 @@ export default class PrometheusMetrics {    loadActiveMetrics() {      this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);      backOff((next, stop) => { -      $.getJSON(this.activeMetricsEndpoint) +      $.ajax({ +        url: this.activeMetricsEndpoint, +        dataType: 'json', +        global: false, +      })          .done((res) => {            if (res && res.success) {              stop(res); diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index 3b920942a3f..632625da8e7 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -1,6 +1,5 @@  /* eslint-disable no-new */ -/* global Flash */ - +import Flash from '../flash';  import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';  export default class ProtectedBranchEdit { @@ -57,7 +56,7 @@ export default class ProtectedBranchEdit {          },        },        error() { -        new Flash('Failed to update branch!', null, $('.js-protected-branches-list')); +        new Flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list'));        },      }).always(() => {        this.$allowedToMergeDropdown.enable(); diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index 09a387c0f9e..dad0ad25b65 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -1,6 +1,5 @@  /* eslint-disable no-new */ -/* global Flash */ - +import Flash from '../flash';  import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';  export default class ProtectedTagEdit { @@ -43,7 +42,7 @@ export default class ProtectedTagEdit {          },        },        error() { -        new Flash('Failed to update tag!', null, $('.js-protected-tags-list')); +        new Flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));        },      }).always(() => {        this.$allowedToCreateDropdownButton.enable(); diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index 41ea9742406..ac1c3ec253c 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -57,7 +57,7 @@        },        showError(message) { -        Flash((errorMessages[message])); +        Flash(errorMessages[message]);        },      },    }; diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 4ce1571b0aa..e917279947e 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -57,7 +57,7 @@        },        showError(message) { -        Flash((errorMessages[message])); +        Flash(errorMessages[message]);        },      },    }; diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js index 34ed40b8b65..795b39bb3dc 100644 --- a/app/assets/javascripts/registry/stores/actions.js +++ b/app/assets/javascripts/registry/stores/actions.js @@ -29,11 +29,9 @@ export const fetchList = ({ commit }, { repo, page }) => {      });  }; -export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath) -  .then(res => res.json()); +export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath); -export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath) -  .then(res => res.json()); +export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath);  export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);  export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING); diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js index e40382e7afc..208c3c39866 100644 --- a/app/assets/javascripts/registry/stores/mutations.js +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -38,7 +38,7 @@ export default {        tag: element.name,        revision: element.revision,        shortRevision: element.short_revision, -      size: element.size, +      size: element.total_size,        layers: element.layers,        location: element.location,        createdAt: element.created_at, diff --git a/app/assets/javascripts/repo/components/new_branch_form.vue b/app/assets/javascripts/repo/components/new_branch_form.vue new file mode 100644 index 00000000000..ba7090e4a9d --- /dev/null +++ b/app/assets/javascripts/repo/components/new_branch_form.vue @@ -0,0 +1,108 @@ +<script> +  import { mapState, mapActions } from 'vuex'; +  import flash, { hideFlash } from '../../flash'; +  import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + +  export default { +    components: { +      loadingIcon, +    }, +    data() { +      return { +        branchName: '', +        loading: false, +      }; +    }, +    computed: { +      ...mapState([ +        'currentBranch', +      ]), +      btnDisabled() { +        return this.loading || this.branchName === ''; +      }, +    }, +    methods: { +      ...mapActions([ +        'createNewBranch', +      ]), +      toggleDropdown() { +        this.$dropdown.dropdown('toggle'); +      }, +      submitNewBranch() { +        // need to query as the element is appended outside of Vue +        const flashEl = this.$refs.flashContainer.querySelector('.flash-alert'); + +        this.loading = true; + +        if (flashEl) { +          hideFlash(flashEl, false); +        } + +        this.createNewBranch(this.branchName) +          .then(() => { +            this.loading = false; +            this.branchName = ''; + +            if (this.dropdownText) { +              this.dropdownText.textContent = this.currentBranch; +            } + +            this.toggleDropdown(); +          }) +          .catch(res => res.json().then((data) => { +            this.loading = false; +            flash(data.message, 'alert', this.$el); +          })); +      }, +    }, +    created() { +      // Dropdown is outside of Vue instance & is controlled by Bootstrap +      this.$dropdown = $('.git-revision-dropdown'); + +      // text element is outside Vue app +      this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text'); +    }, +  }; +</script> + +<template> +  <div> +    <div +      class="flash-container" +      ref="flashContainer" +    > +    </div> +    <p> +      Create from: +      <code>{{ currentBranch }}</code> +    </p> +    <input +      class="form-control js-new-branch-name" +      type="text" +      placeholder="Name new branch" +      v-model="branchName" +      @keyup.enter.stop.prevent="submitNewBranch" +    /> +    <div class="prepend-top-default clearfix"> +      <button +        type="button" +        class="btn btn-primary pull-left" +        :disabled="btnDisabled" +        @click.stop.prevent="submitNewBranch" +      > +        <loading-icon +          v-if="loading" +          :inline="true" +        /> +        <span>Create</span> +      </button> +      <button +        type="button" +        class="btn btn-default pull-right" +        @click.stop.prevent="toggleDropdown" +      > +        Cancel +      </button> +    </div> +  </div> +</template> diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue new file mode 100644 index 00000000000..a5ee4f71281 --- /dev/null +++ b/app/assets/javascripts/repo/components/new_dropdown/index.vue @@ -0,0 +1,84 @@ +<script> +  import { mapState } from 'vuex'; +  import newModal from './modal.vue'; +  import upload from './upload.vue'; + +  export default { +    components: { +      newModal, +      upload, +    }, +    data() { +      return { +        openModal: false, +        modalType: '', +      }; +    }, +    computed: { +      ...mapState([ +        'path', +      ]), +    }, +    methods: { +      createNewItem(type) { +        this.modalType = type; +        this.toggleModalOpen(); +      }, +      toggleModalOpen() { +        this.openModal = !this.openModal; +      }, +    }, +  }; +</script> + +<template> +  <div> +    <ul class="breadcrumb repo-breadcrumb"> +      <li class="dropdown"> +        <button +          type="button" +          class="btn btn-default dropdown-toggle add-to-tree" +          data-toggle="dropdown" +          aria-label="Create new file or directory" +        > +          <i +            class="fa fa-plus" +            aria-hidden="true" +          > +          </i> +        </button> +        <ul class="dropdown-menu"> +          <li> +            <a +              href="#" +              role="button" +              @click.prevent="createNewItem('blob')" +            > +              {{ __('New file') }} +            </a> +          </li> +          <li> +            <upload +              :path="path" +            /> +          </li> +          <li> +            <a +              href="#" +              role="button" +              @click.prevent="createNewItem('tree')" +            > +              {{ __('New directory') }} +            </a> +          </li> +        </ul> +      </li> +    </ul> +    <new-modal +      v-if="openModal" +      :type="modalType" +      :path="path" +      @toggle="toggleModalOpen" +    /> +  </div> +</template> diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/repo/components/new_dropdown/modal.vue new file mode 100644 index 00000000000..ac1f613bb71 --- /dev/null +++ b/app/assets/javascripts/repo/components/new_dropdown/modal.vue @@ -0,0 +1,98 @@ +<script> +  import { mapActions } from 'vuex'; +  import { __ } from '../../../locale'; +  import popupDialog from '../../../vue_shared/components/popup_dialog.vue'; + +  export default { +    props: { +      type: { +        type: String, +        required: true, +      }, +      path: { +        type: String, +        required: true, +      }, +    }, +    data() { +      return { +        entryName: this.path !== '' ? `${this.path}/` : '', +      }; +    }, +    components: { +      popupDialog, +    }, +    methods: { +      ...mapActions([ +        'createTempEntry', +      ]), +      createEntryInStore() { +        this.createTempEntry({ +          name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), +          type: this.type, +        }); + +        this.toggleModalOpen(); +      }, +      toggleModalOpen() { +        this.$emit('toggle'); +      }, +    }, +    computed: { +      modalTitle() { +        if (this.type === 'tree') { +          return __('Create new directory'); +        } + +        return __('Create new file'); +      }, +      buttonLabel() { +        if (this.type === 'tree') { +          return __('Create directory'); +        } + +        return __('Create file'); +      }, +      formLabelName() { +        if (this.type === 'tree') { +          return __('Directory name'); +        } + +        return __('File name'); +      }, +    }, +    mounted() { +      this.$refs.fieldName.focus(); +    }, +  }; +</script> + +<template> +  <popup-dialog +    :title="modalTitle" +    :primary-button-label="buttonLabel" +    kind="success" +    @toggle="toggleModalOpen" +    @submit="createEntryInStore" +  > +    <form +      class="form-horizontal" +      slot="body" +      @submit.prevent="createEntryInStore" +    > +      <fieldset class="form-group append-bottom-0"> +        <label class="label-light col-sm-3"> +          {{ formLabelName }} +        </label> +        <div class="col-sm-9"> +          <input +            type="text" +            class="form-control" +            v-model="entryName" +            ref="fieldName" +          /> +        </div> +      </fieldset> +    </form> +  </popup-dialog> +</template> diff --git a/app/assets/javascripts/repo/components/new_dropdown/upload.vue b/app/assets/javascripts/repo/components/new_dropdown/upload.vue new file mode 100644 index 00000000000..14ad32f4ae0 --- /dev/null +++ b/app/assets/javascripts/repo/components/new_dropdown/upload.vue @@ -0,0 +1,68 @@ +<script> +  import { mapActions } from 'vuex'; + +  export default { +    props: { +      path: { +        type: String, +        required: true, +      }, +    }, +    methods: { +      ...mapActions([ +        'createTempEntry', +      ]), +      createFile(target, file, isText) { +        const { name } = file; +        let { result } = target; + +        if (!isText) { +          result = result.split('base64,')[1]; +        } + +        this.createTempEntry({ +          name, +          type: 'blob', +          content: result, +          base64: !isText, +        }); +      }, +      readFile(file) { +        const reader = new FileReader(); +        const isText = file.type.match(/text.*/) !== null; + +        reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true }); + +        if (isText) { +          reader.readAsText(file); +        } else { +          reader.readAsDataURL(file); +        } +      }, +      openFile() { +        Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file)); +      }, +    }, +    mounted() { +      this.$refs.fileUpload.addEventListener('change', this.openFile); +    }, +    beforeDestroy() { +      this.$refs.fileUpload.removeEventListener('change', this.openFile); +    }, +  }; +</script> + +<template> +  <label +    role="button" +    class="menu-item" +  > +    {{ __('Upload file') }} +    <input +      id="file-upload" +      type="file" +      class="hidden" +      ref="fileUpload" +    /> +  </label> +</template> diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue index cc60aa5939c..98117802016 100644 --- a/app/assets/javascripts/repo/components/repo.vue +++ b/app/assets/javascripts/repo/components/repo.vue @@ -1,70 +1,59 @@  <script> +import { mapState, mapGetters } from 'vuex';  import RepoSidebar from './repo_sidebar.vue';  import RepoCommitSection from './repo_commit_section.vue';  import RepoTabs from './repo_tabs.vue';  import RepoFileButtons from './repo_file_buttons.vue';  import RepoPreview from './repo_preview.vue'; -import RepoMixin from '../mixins/repo_mixin'; -import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; -import Store from '../stores/repo_store'; -import Helper from '../helpers/repo_helper'; -import MonacoLoaderHelper from '../helpers/monaco_loader_helper'; +import repoEditor from './repo_editor.vue';  export default { -  data: () => Store, -  mixins: [RepoMixin], +  computed: { +    ...mapState([ +      'currentBlobView', +    ]), +    ...mapGetters([ +      'isCollapsed', +      'changedFiles', +    ]), +  },    components: {      RepoSidebar,      RepoTabs,      RepoFileButtons, -    'repo-editor': MonacoLoaderHelper.repoEditorLoader, +    repoEditor,      RepoCommitSection, -    PopupDialog,      RepoPreview,    }, -    mounted() { -    Helper.getContent().catch(Helper.loadingError); -  }, - -  methods: { -    toggleDialogOpen(toggle) { -      this.dialog.open = toggle; -    }, - -    dialogSubmitted(status) { -      this.toggleDialogOpen(false); -      this.dialog.status = status; -    }, +    const returnValue = 'Are you sure you want to lose unsaved changes?'; +    window.onbeforeunload = (e) => { +      if (!this.changedFiles.length) return undefined; -    toggleBlobView: Store.toggleBlobView, +      Object.assign(e, { +        returnValue, +      }); +      return returnValue; +    };    },  };  </script>  <template>    <div class="repository-view"> -    <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isMini}"> +    <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isCollapsed}">        <repo-sidebar/> -      <div v-if="isMini" -      class="panel-right" -      :class="{'edit-mode': editMode}"> +      <div +        v-if="isCollapsed" +        class="panel-right" +      >          <repo-tabs/>          <component            :is="currentBlobView" -          class="blob-viewer-container"/> +        />          <repo-file-buttons/>        </div>      </div> -    <repo-commit-section/> -    <popup-dialog -      v-show="dialog.open" -      :primary-button-label="__('Discard changes')" -      kind="warning" -      :title="__('Are you sure?')" -      :text="__('Are you sure you want to discard your changes?')" -      @toggle="toggleDialogOpen" -      @submit="dialogSubmitted" -    /> +    <repo-commit-section v-if="changedFiles.length" />    </div>  </template> diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index 119e38c583d..377e3d65348 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -1,70 +1,100 @@  <script> -/* global Flash */ -import Store from '../stores/repo_store'; -import RepoMixin from '../mixins/repo_mixin'; -import Service from '../services/repo_service'; +import { mapGetters, mapState, mapActions } from 'vuex'; +import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; +import { n__ } from '../../locale';  export default { -  data: () => Store, - -  mixins: [RepoMixin], - +  components: { +    PopupDialog, +  }, +  data() { +    return { +      showNewBranchDialog: false, +      submitCommitsLoading: false, +      startNewMR: false, +      commitMessage: '', +    }; +  },    computed: { -    showCommitable() { -      return this.isCommitable && this.changedFiles.length; -    }, - -    branchPaths() { -      return this.changedFiles.map(f => f.path); -    }, - -    cantCommitYet() { +    ...mapState([ +      'currentBranch', +    ]), +    ...mapGetters([ +      'changedFiles', +    ]), +    commitButtonDisabled() {        return !this.commitMessage || this.submitCommitsLoading;      }, - -    filePluralize() { -      return this.changedFiles.length > 1 ? 'files' : 'file'; +    commitButtonText() { +      return n__('Commit %d file', 'Commit %d files', this.changedFiles.length);      },    }, -    methods: { -    makeCommit() { -      // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions -      const commitMessage = this.commitMessage; -      const actions = this.changedFiles.map(f => ({ -        action: 'update', -        file_path: f.path, -        content: f.newContent, -      })); +    ...mapActions([ +      'checkCommitStatus', +      'commitChanges', +      'getTreeData', +    ]), +    makeCommit(newBranch = false) { +      const createNewBranch = newBranch || this.startNewMR; +        const payload = { -        branch: Store.currentBranch, -        commit_message: commitMessage, -        actions, +        branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch, +        commit_message: this.commitMessage, +        actions: this.changedFiles.map(f => ({ +          action: f.tempFile ? 'create' : 'update', +          file_path: f.path, +          content: f.content, +          encoding: f.base64 ? 'base64' : 'text', +        })), +        start_branch: createNewBranch ? this.currentBranch : undefined,        }; -      Store.submitCommitsLoading = true; -      Service.commitFiles(payload) -        .then(this.resetCommitState) -        .catch(() => Flash('An error occurred while committing your changes')); + +      this.showNewBranchDialog = false; +      this.submitCommitsLoading = true; + +      this.commitChanges({ payload, newMr: this.startNewMR }) +        .then(() => { +          this.submitCommitsLoading = false; +          this.getTreeData(); +        }) +        .catch(() => { +          this.submitCommitsLoading = false; +        });      }, +    tryCommit() { +      this.submitCommitsLoading = true; -    resetCommitState() { -      this.submitCommitsLoading = false; -      this.changedFiles = []; -      this.commitMessage = ''; -      this.editMode = false; -      window.scrollTo(0, 0); +      this.checkCommitStatus() +        .then((branchChanged) => { +          if (branchChanged) { +            this.showNewBranchDialog = true; +          } else { +            this.makeCommit(); +          } +        }) +        .catch(() => { +          this.submitCommitsLoading = false; +        });      },    },  };  </script>  <template> -<div -  v-if="showCommitable" -  id="commit-area"> +<div id="commit-area"> +  <popup-dialog +    v-if="showNewBranchDialog" +    :primary-button-label="__('Create new branch')" +    kind="primary" +    :title="__('Branch has changed')" +    :text="__('This branch has changed since you started editing. Would you like to create a new branch?')" +    @toggle="showNewBranchDialog = false" +    @submit="makeCommit(true)" +  />    <form      class="form-horizontal" -    @submit.prevent="makeCommit"> +    @submit.prevent="tryCommit()">      <fieldset>        <div class="form-group">          <label class="col-md-4 control-label staged-files"> @@ -73,10 +103,10 @@ export default {          <div class="col-md-6">            <ul class="list-unstyled changed-files">              <li -              v-for="branchPath in branchPaths" -              :key="branchPath"> +              v-for="(file, index) in changedFiles" +              :key="index">                <span class="help-block"> -                {{branchPath}} +                {{ file.path }}                </span>              </li>            </ul> @@ -111,21 +141,28 @@ export default {        </div>        <div class="col-md-offset-4 col-md-6">          <button -          ref="submitCommit"            type="submit" -          :disabled="cantCommitYet" +          :disabled="commitButtonDisabled"            class="btn btn-success">            <i              v-if="submitCommitsLoading" -            class="fa fa-spinner fa-spin" +            class="js-commit-loading-icon fa fa-spinner fa-spin"              aria-hidden="true"              aria-label="loading">            </i>            <span class="commit-summary"> -            Commit {{changedFiles.length}} {{filePluralize}} +            {{ commitButtonText }}            </span>          </button>        </div> +      <div class="col-md-offset-4 col-md-6"> +        <div class="checkbox"> +          <label> +            <input type="checkbox" v-model="startNewMR"> +            <span>Start a <strong>new merge request</strong> with these changes</span> +          </label> +        </div> +      </div>      </fieldset>    </form>  </div> diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue index 353142edeb7..6c1bb4b8566 100644 --- a/app/assets/javascripts/repo/components/repo_edit_button.vue +++ b/app/assets/javascripts/repo/components/repo_edit_button.vue @@ -1,48 +1,57 @@  <script> -import Store from '../stores/repo_store'; -import RepoMixin from '../mixins/repo_mixin'; +import { mapGetters, mapActions, mapState } from 'vuex'; +import popupDialog from '../../vue_shared/components/popup_dialog.vue';  export default { -  data: () => Store, -  mixins: [RepoMixin], +  components: { +    popupDialog, +  },    computed: { +    ...mapState([ +      'editMode', +      'discardPopupOpen', +    ]), +    ...mapGetters([ +      'canEditFile', +    ]),      buttonLabel() {        return this.editMode ? this.__('Cancel edit') : this.__('Edit');      }, - -    showButton() { -      return this.isCommitable && -        !this.activeFile.render_error && -        !this.binary && -        this.openedFiles.length; -    },    },    methods: { -    editCancelClicked() { -      if (this.changedFiles.length) { -        this.dialog.open = true; -        return; -      } -      this.editMode = !this.editMode; -      Store.toggleBlobView(); -    }, +    ...mapActions([ +      'toggleEditMode', +      'closeDiscardPopup', +    ]),    },  };  </script>  <template> -<button -  v-if="showButton" -  class="btn btn-default" -  type="button" -  @click.prevent="editCancelClicked"> -  <i -    v-if="!editMode" -    class="fa fa-pencil" -    aria-hidden="true"> -  </i> -  <span> -    {{buttonLabel}} -  </span> -</button> +  <div class="editable-mode"> +    <button +      v-if="canEditFile" +      class="btn btn-default" +      type="button" +      @click.prevent="toggleEditMode()"> +      <i +        v-if="!editMode" +        class="fa fa-pencil" +        aria-hidden="true"> +      </i> +      <span> +        {{buttonLabel}} +      </span> +    </button> +    <popup-dialog +      v-if="discardPopupOpen" +      class="text-left" +      :primary-button-label="__('Discard changes')" +      kind="warning" +      :title="__('Are you sure?')" +      :text="__('Are you sure you want to discard your changes?')" +      @toggle="closeDiscardPopup" +      @submit="toggleEditMode(true)" +    /> +  </div>  </template> diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index 02d9c775046..1c864b176b1 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -1,121 +1,107 @@  <script>  /* global monaco */ -import Store from '../stores/repo_store'; -import Service from '../services/repo_service'; -import Helper from '../helpers/repo_helper'; - -const RepoEditor = { -  data: () => Store, +import { mapGetters, mapActions } from 'vuex'; +import flash from '../../flash'; +import monacoLoader from '../monaco_loader'; +export default {    destroyed() { -    if (Helper.monacoInstance) { -      Helper.monacoInstance.destroy(); +    if (this.monacoInstance) { +      this.monacoInstance.destroy();      }    }, -    mounted() { -    Service.getRaw(this.activeFile.raw_path) -      .then((rawResponse) => { -        Store.blobRaw = rawResponse.data; -        Store.activeFile.plain = rawResponse.data; +    if (this.monaco) { +      this.initMonaco(); +    } else { +      monacoLoader(['vs/editor/editor.main'], () => { +        this.monaco = monaco; + +        this.initMonaco(); +      }); +    } +  }, +  methods: { +    ...mapActions([ +      'getRawFileData', +      'changeFileContent', +    ]), +    initMonaco() { +      if (this.shouldHideEditor) return; + +      if (this.monacoInstance) { +        this.monacoInstance.setModel(null); +      } -        const monacoInstance = Helper.monaco.editor.create(this.$el, { -          model: null, -          readOnly: false, -          contextmenu: false, -        }); +      this.getRawFileData(this.activeFile) +        .then(() => { +          if (!this.monacoInstance) { +            this.monacoInstance = this.monaco.editor.create(this.$el, { +              model: null, +              readOnly: false, +              contextmenu: true, +              scrollBeyondLastLine: false, +            }); -        Helper.monacoInstance = monacoInstance; +            this.languages = this.monaco.languages.getLanguages(); -        this.addMonacoEvents(); +            this.addMonacoEvents(); +          } -        this.setupEditor(); -      }) -      .catch(Helper.loadingError); -  }, - -  methods: { +          this.setupEditor(); +        }) +        .catch(() => flash('Error setting up monaco. Please try again.')); +    },      setupEditor() { -      this.showHide(); +      if (!this.activeFile) return; +      const content = this.activeFile.content !== '' ? this.activeFile.content : this.activeFile.raw; -      Helper.setMonacoModelFromLanguage(); -    }, +      const foundLang = this.languages.find(lang => +        lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0, +      ); +      const newModel = this.monaco.editor.createModel( +        content, foundLang ? foundLang.id : 'plaintext', +      ); -    showHide() { -      if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) { -        this.$el.style.display = 'none'; -      } else { -        this.$el.style.display = 'inline-block'; -      } +      this.monacoInstance.setModel(newModel);      }, -      addMonacoEvents() { -      Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); -      Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); -    }, - -    onMonacoEditorKeysPressed() { -      Store.setActiveFileContents(Helper.monacoInstance.getValue()); -    }, - -    onMonacoEditorMouseUp(e) { -      if (!e.target.position) return; -      const lineNumber = e.target.position.lineNumber; -      if (e.target.element.classList.contains('line-numbers')) { -        location.hash = `L${lineNumber}`; -        Store.setActiveLine(lineNumber); -      } +      this.monacoInstance.onKeyUp(() => { +        this.changeFileContent({ +          file: this.activeFile, +          content: this.monacoInstance.getValue(), +        }); +      });      },    }, -    watch: { -    dialog: { -      handler(obj) { -        const newObj = obj; -        if (newObj.status) { -          newObj.status = false; -          this.openedFiles = this.openedFiles.map((file) => { -            const f = file; -            if (f.active) { -              this.blobRaw = f.plain; -            } -            f.changed = false; -            delete f.newContent; - -            return f; -          }); -          this.editMode = false; -          Store.toggleBlobView(); -        } -      }, -      deep: true, -    }, - -    blobRaw() { -      if (Helper.monacoInstance && !this.isTree) { -        this.setupEditor(); -      } -    }, - -    activeLine() { -      if (Helper.monacoInstance) { -        Helper.monacoInstance.setPosition({ -          lineNumber: this.activeLine, -          column: 1, -        }); +    activeFile(oldVal, newVal) { +      if (newVal && !newVal.active) { +        this.initMonaco();        }      },    },    computed: { +    ...mapGetters([ +      'activeFile', +      'activeFileExtension', +    ]),      shouldHideEditor() { -      return !this.openedFiles.length || (this.binary && !this.activeFile.raw); +      return this.activeFile.binary && !this.activeFile.raw;      },    },  }; - -export default RepoEditor;  </script>  <template> -<div id="ide" v-if='!shouldHideEditor'></div> +  <div +    id="ide" +    class="blob-viewer-container blob-editor-container" +  > +    <div +      v-if="shouldHideEditor" +      v-html="activeFile.html" +    > +    </div> +  </div>  </template> diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue index 8b9cbd23456..5be47d568e7 100644 --- a/app/assets/javascripts/repo/components/repo_file.vue +++ b/app/assets/javascripts/repo/components/repo_file.vue @@ -1,107 +1,118 @@  <script> -import TimeAgoMixin from '../../vue_shared/mixins/timeago'; +  import { mapActions, mapGetters } from 'vuex'; +  import timeAgoMixin from '../../vue_shared/mixins/timeago'; +  import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; -const RepoFile = { -  mixins: [TimeAgoMixin], -  props: { -    file: { -      type: Object, -      required: true, +  export default { +    mixins: [ +      timeAgoMixin, +    ], +    components: { +      skeletonLoadingContainer,      }, -    isMini: { -      type: Boolean, -      required: false, -      default: false, +    props: { +      file: { +        type: Object, +        required: true, +      },      }, -    loading: { -      type: Object, -      required: false, -      default() { return { tree: false }; }, +    computed: { +      ...mapGetters([ +        'isCollapsed', +      ]), +      isSubmodule() { +        return this.file.type === 'submodule'; +      }, +      fileIcon() { +        return { +          'fa-spinner fa-spin': this.file.loading, +          [this.file.icon]: !this.file.loading, +          'fa-folder-open': !this.file.loading && this.file.opened, +        }; +      }, +      levelIndentation() { +        return { +          marginLeft: `${this.file.level * 16}px`, +        }; +      }, +      shortId() { +        return this.file.id.substr(0, 8); +      }, +      submoduleColSpan() { +        return !this.isCollapsed && this.isSubmodule ? 3 : 1; +      },      }, -    hasFiles: { -      type: Boolean, -      required: false, -      default: false, +    methods: { +      ...mapActions([ +        'clickedTreeRow', +      ]),      }, -    activeFile: { -      type: Object, -      required: true, -    }, -  }, - -  computed: { -    canShowFile() { -      return !this.loading.tree || this.hasFiles; -    }, - -    fileIcon() { -      const classObj = { -        'fa-spinner fa-spin': this.file.loading, -        [this.file.icon]: !this.file.loading, -      }; -      return classObj; -    }, - -    fileIndentation() { -      return { -        'margin-left': `${this.file.level * 10}px`, -      }; -    }, - -    activeFileClass() { -      return { -        active: this.activeFile.url === this.file.url, -      }; -    }, -  }, - -  methods: { -    linkClicked(file) { -      this.$emit('linkclicked', file); -    }, -  }, -}; - -export default RepoFile; +  };  </script>  <template> -<tr -  v-if="canShowFile" -  class="file" -  :class="activeFileClass" -  @click.prevent="linkClicked(file)"> -  <td> -    <i -      class="fa fa-fw file-icon" -      :class="fileIcon" -      :style="fileIndentation" -      aria-label="file icon"> -    </i> -    <a -      :href="file.url" -      class="repo-file-name" -      :title="file.url"> -      {{file.name}} -    </a> -  </td> +  <tr +    class="file" +    @click.prevent="clickedTreeRow(file)"> +    <td +      class="multi-file-table-col-name" +      :colspan="submoduleColSpan" +    > +      <i +        class="fa fa-fw file-icon" +        :class="fileIcon" +        :style="levelIndentation" +        aria-hidden="true" +      > +      </i> +      <a +        :href="file.url" +        class="repo-file-name" +      > +        {{ file.name }} +      </a> +      <template v-if="isSubmodule && file.id"> +        @ +        <span class="commit-sha"> +          <a +            @click.stop +            :href="file.tree_url" +          > +            {{ shortId }} +          </a> +        </span> +      </template> +    </td> -  <template v-if="!isMini"> -    <td class="hidden-sm hidden-xs"> -      <div class="commit-message"> -        <a @click.stop :href="file.lastCommitUrl"> -          {{file.lastCommitMessage}} +    <template v-if="!isCollapsed && !isSubmodule"> +      <td class="hidden-sm hidden-xs"> +        <a +          v-if="file.lastCommit.message" +          @click.stop +          :href="file.lastCommit.url" +          class="commit-message" +        > +          {{ file.lastCommit.message }}          </a> -      </div> -    </td> +        <skeleton-loading-container +          v-else +          :small="true" +        /> +      </td> -    <td class="hidden-xs text-right"> -      <span -        class="commit-update" -        :title="tooltipTitle(file.lastCommitUpdate)"> -        {{timeFormated(file.lastCommitUpdate)}} -      </span> -    </td> -  </template> -</tr> +      <td class="commit-update hidden-xs text-right"> +        <span +          v-if="file.lastCommit.updatedAt" +          :title="tooltipTitle(file.lastCommit.updatedAt)" +        > +          {{ timeFormated(file.lastCommit.updatedAt) }} +        </span> +        <skeleton-loading-container +          v-else +          class="animation-container-right" +          :small="true" +        /> +      </td> +    </template> +  </tr>  </template> diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue index e43ef366f47..dd948ee84fb 100644 --- a/app/assets/javascripts/repo/components/repo_file_buttons.vue +++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue @@ -1,40 +1,35 @@  <script> -import Store from '../stores/repo_store'; -import Helper from '../helpers/repo_helper'; -import RepoMixin from '../mixins/repo_mixin'; - -const RepoFileButtons = { -  data: () => Store, - -  mixins: [RepoMixin], +import { mapGetters } from 'vuex'; +export default {    computed: { - -    rawDownloadButtonLabel() { -      return this.binary ? 'Download' : 'Raw'; +    ...mapGetters([ +      'activeFile', +    ]), +    showButtons() { +      return this.activeFile.rawPath || +        this.activeFile.blamePath || +        this.activeFile.commitsPath || +        this.activeFile.permalink;      }, - -    canPreview() { -      return Helper.isRenderable(); +    rawDownloadButtonLabel() { +      return this.activeFile.binary ? 'Download' : 'Raw';      },    }, - -  methods: { -    rawPreviewToggle: Store.toggleRawPreview, -  },  }; - -export default RepoFileButtons;  </script>  <template> -  <div id="repo-file-buttons"> +  <div +    v-if="showButtons" +    class="repo-file-buttons" +  >      <a -      :href="activeFile.raw_path" +      :href="activeFile.rawPath"        target="_blank"        class="btn btn-default raw"        rel="noopener noreferrer"> -      {{rawDownloadButtonLabel}} +      {{ rawDownloadButtonLabel }}      </a>      <div @@ -42,12 +37,12 @@ export default RepoFileButtons;        role="group"        aria-label="File actions">        <a -        :href="activeFile.blame_path" +        :href="activeFile.blamePath"          class="btn btn-default blame">          Blame        </a>        <a -        :href="activeFile.commits_path" +        :href="activeFile.commitsPath"          class="btn btn-default history">          History        </a> @@ -57,13 +52,5 @@ export default RepoFileButtons;          Permalink        </a>      </div> - -    <a -      v-if="canPreview" -      href="#" -      @click.prevent="rawPreviewToggle" -      class="btn btn-default preview"> -      {{activeFileLabel}} -    </a>    </div>  </template> diff --git a/app/assets/javascripts/repo/components/repo_file_options.vue b/app/assets/javascripts/repo/components/repo_file_options.vue deleted file mode 100644 index 6a15755f029..00000000000 --- a/app/assets/javascripts/repo/components/repo_file_options.vue +++ /dev/null @@ -1,25 +0,0 @@ -<script> -const RepoFileOptions = { -  props: { -    isMini: { -      type: Boolean, -      required: false, -      default: false, -    }, -    projectName: { -      type: String, -      required: true, -    }, -  }, -}; - -export default RepoFileOptions; -</script> - -<template> -  <tr v-if="isMini" class="repo-file-options"> -    <td> -      <span class="title">{{projectName}}</span> -    </td> -  </tr> -</template> diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue index bc8c64c8362..8fa637d771f 100644 --- a/app/assets/javascripts/repo/components/repo_loading_file.vue +++ b/app/assets/javascripts/repo/components/repo_loading_file.vue @@ -1,76 +1,44 @@  <script> -const RepoLoadingFile = { -  props: { -    loading: { -      type: Object, -      required: false, -      default: {}, -    }, -    hasFiles: { -      type: Boolean, -      required: false, -      default: false, -    }, -    isMini: { -      type: Boolean, -      required: false, -      default: false, -    }, -  }, +  import { mapGetters } from 'vuex'; +  import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; -  computed: { -    showGhostLines() { -      return this.loading.tree && !this.hasFiles; +  export default { +    components: { +      skeletonLoadingContainer,      }, -  }, - -  methods: { -    lineOfCode(n) { -      return `skeleton-line-${n}`; +    computed: { +      ...mapGetters([ +        'isCollapsed', +      ]),      }, -  }, -}; - -export default RepoLoadingFile; +  };  </script>  <template>    <tr -    v-if="showGhostLines" -    class="loading-file"> -    <td> -      <div -        class="animation-container animation-container-small"> -        <div -          v-for="n in 6" -          :key="n" -          :class="lineOfCode(n)"> -        </div> -      </div> -    </td> - -    <td -      v-if="!isMini" -      class="hidden-sm hidden-xs"> -      <div class="animation-container"> -        <div -          v-for="n in 6" -          :key="n" -          :class="lineOfCode(n)"> -        </div> -      </div> +    class="loading-file" +    aria-label="Loading files" +  > +    <td class="multi-file-table-col-name"> +      <skeleton-loading-container +        :small="true" +      />      </td> +    <template v-if="!isCollapsed"> +      <td +        class="hidden-sm hidden-xs"> +        <skeleton-loading-container +          :small="true" +        /> +      </td> -    <td -      v-if="!isMini" -      class="hidden-xs"> -      <div class="animation-container animation-container-small"> -        <div -          v-for="n in 6" -          :key="n" -          :class="lineOfCode(n)"> -        </div> -      </div> -    </td> +      <td +        class="hidden-xs"> +        <skeleton-loading-container +          class="animation-container-right" +          :small="true" +        /> +      </td> +    </template>    </tr>  </template> diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue index bbdbdc61e38..a2b305bbd05 100644 --- a/app/assets/javascripts/repo/components/repo_prev_directory.vue +++ b/app/assets/javascripts/repo/components/repo_prev_directory.vue @@ -1,38 +1,34 @@  <script> -import RepoMixin from '../mixins/repo_mixin'; +  import { mapGetters, mapState, mapActions } from 'vuex'; -const RepoPreviousDirectory = { -  props: { -    prevUrl: { -      type: String, -      required: true, +  export default { +    computed: { +      ...mapState([ +        'parentTreeUrl', +      ]), +      ...mapGetters([ +        'isCollapsed', +      ]), +      colSpanCondition() { +        return this.isCollapsed ? undefined : 3; +      },      }, -  }, - -  mixins: [RepoMixin], - -  computed: { -    colSpanCondition() { -      return this.isMini ? undefined : 3; +    methods: { +      ...mapActions([ +        'getTreeData', +      ]),      }, -  }, - -  methods: { -    linkClicked(file) { -      this.$emit('linkclicked', file); -    }, -  }, -}; - -export default RepoPreviousDirectory; +  };  </script>  <template> -<tr class="prev-directory"> -  <td -    :colspan="colSpanCondition" -    @click.prevent="linkClicked(prevUrl)"> -    <a :href="prevUrl">..</a> -  </td> -</tr> +  <tr class="file prev-directory"> +    <td +      :colspan="colSpanCondition" +      class="table-cell" +      @click.prevent="getTreeData({ endpoint: parentTreeUrl })" +    > +      <a :href="parentTreeUrl">...</a> +    </td> +  </tr>  </template> diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index a87bef6084a..d1883299bd9 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -1,24 +1,20 @@  <script>  /* global LineHighlighter */ - -import Store from '../stores/repo_store'; +import { mapGetters } from 'vuex';  export default { -  data: () => Store,    computed: { -    html() { -      return this.activeFile.html; +    ...mapGetters([ +      'activeFile', +    ]), +    renderErrorTooLarge() { +      return this.activeFile.renderError === 'too_large';      },    },    methods: {      highlightFile() {        $(this.$el).find('.file-content').syntaxHighlight();      }, -    highlightLine() { -      if (Store.activeLine > -1) { -        this.lineHighlighter.highlightHash(`#L${Store.activeLine}`); -      } -    },    },    mounted() {      this.highlightFile(); @@ -27,38 +23,39 @@ export default {        scrollFileHolder: true,      });    }, -  watch: { -    html() { -      this.$nextTick(() => { -        this.highlightFile(); -        this.highlightLine(); -      }); -    }, -    activeLine() { -      this.highlightLine(); -    }, +  updated() { +    this.$nextTick(() => { +      this.highlightFile(); +    });    },  };  </script>  <template> -<div> +<div class="blob-viewer-container">    <div -    v-if="!activeFile.render_error" +    v-if="!activeFile.renderError"      v-html="activeFile.html">    </div>    <div -    v-else-if="activeFile.tooLarge" +    v-else-if="activeFile.tempFile" +    class="vertical-center render-error"> +    <p class="text-center"> +      The source could not be displayed for this temporary file. +    </p> +  </div> +  <div +    v-else-if="renderErrorTooLarge"      class="vertical-center render-error">      <p class="text-center"> -      The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead. +      The source could not be displayed because it is too large. You can <a :href="activeFile.rawPath" download>download</a> it instead.      </p>    </div>    <div      v-else      class="vertical-center render-error">      <p class="text-center"> -      The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.raw_path">download</a> it instead. +      The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.rawPath" download>download</a> it instead.      </p>    </div>  </div> diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index e0f3c33003a..9365b09326f 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -1,129 +1,87 @@  <script> -import Service from '../services/repo_service'; -import Helper from '../helpers/repo_helper'; -import Store from '../stores/repo_store'; +import { mapState, mapGetters, mapActions } from 'vuex';  import RepoPreviousDirectory from './repo_prev_directory.vue'; -import RepoFileOptions from './repo_file_options.vue';  import RepoFile from './repo_file.vue';  import RepoLoadingFile from './repo_loading_file.vue'; -import RepoMixin from '../mixins/repo_mixin';  export default { -  mixins: [RepoMixin],    components: { -    'repo-file-options': RepoFileOptions,      'repo-previous-directory': RepoPreviousDirectory,      'repo-file': RepoFile,      'repo-loading-file': RepoLoadingFile,    }, -    created() { -    window.addEventListener('popstate', this.checkHistory); +    window.addEventListener('popstate', this.popHistoryState);    },    destroyed() { -    window.removeEventListener('popstate', this.checkHistory); +    window.removeEventListener('popstate', this.popHistoryState); +  }, +  mounted() { +    this.getTreeData(); +  }, +  computed: { +    ...mapState([ +      'loading', +      'isRoot', +    ]), +    ...mapState({ +      projectName(state) { +        return state.project.name; +      }, +    }), +    ...mapGetters([ +      'treeList', +      'isCollapsed', +    ]),    }, - -  data: () => Store, -    methods: { -    checkHistory() { -      let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1); -      if (!selectedFile) { -        // Maybe it is not in the current tree but in the opened tabs -        selectedFile = Helper.getFileFromPath(location.pathname); -      } - -      let lineNumber = null; -      if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2)); - -      if (selectedFile) { -        if (selectedFile.url !== this.activeFile.url) { -          this.fileClicked(selectedFile, lineNumber); -        } else { -          Store.setActiveLine(lineNumber); -        } -      } else { -        // Not opened at all lets open new tab -        this.fileClicked({ -          url: location.href, -        }, lineNumber); -      } -    }, - -    fileClicked(clickedFile, lineNumber) { -      let file = clickedFile; -      if (file.loading) return; -      file.loading = true; - -      if (file.type === 'tree' && file.opened) { -        file = Store.removeChildFilesOfTree(file); -        file.loading = false; -        Store.setActiveLine(lineNumber); -      } else { -        const openFile = Helper.getFileFromPath(file.url); -        if (openFile) { -          file.loading = false; -          Store.setActiveFiles(openFile); -          Store.setActiveLine(lineNumber); -        } else { -          Service.url = file.url; -          Helper.getContent(file) -            .then(() => { -              file.loading = false; -              Helper.scrollTabsRight(); -              Store.setActiveLine(lineNumber); -            }) -            .catch(Helper.loadingError); -        } -      } -    }, - -    goToPreviousDirectoryClicked(prevURL) { -      Service.url = prevURL; -      Helper.getContent(null) -        .then(() => Helper.scrollTabsRight()) -        .catch(Helper.loadingError); -    }, +    ...mapActions([ +      'getTreeData', +      'popHistoryState', +    ]),    },  };  </script>  <template> -<div id="sidebar" :class="{'sidebar-mini' : isMini}"> +<div id="sidebar" :class="{'sidebar-mini' : isCollapsed}">    <table class="table"> -    <thead v-if="!isMini"> +    <thead>        <tr> -        <th class="name">Name</th> -        <th class="hidden-sm hidden-xs last-commit">Last commit</th> -        <th class="hidden-xs last-update text-right">Last update</th> +        <th +          v-if="isCollapsed" +          class="repo-file-options title" +        > +          <strong class="clgray"> +            {{ projectName }} +          </strong> +        </th> +        <template v-else> +          <th class="name multi-file-table-col-name"> +            Name +          </th> +          <th class="hidden-sm hidden-xs last-commit"> +            Last commit +          </th> +          <th class="hidden-xs last-update text-right"> +            Last update +          </th> +        </template>        </tr>      </thead>      <tbody> -      <repo-file-options -        :is-mini="isMini" -        :project-name="projectName" -      />        <repo-previous-directory -        v-if="isRoot" -        :prev-url="prevURL" -        @linkclicked="goToPreviousDirectoryClicked(prevURL)"/> +        v-if="!isRoot && treeList.length" +      />        <repo-loading-file +        v-if="!treeList.length && loading"          v-for="n in 5"          :key="n" -        :loading="loading" -        :has-files="!!files.length" -        :is-mini="isMini"        />        <repo-file -        v-for="file in files" -        :key="file.id" +        v-for="(file, index) in treeList" +        :key="file.key"          :file="file" -        :is-mini="isMini" -        @linkclicked="fileClicked(file)" -        :is-tree="isTree" -        :has-files="!!files.length" -        :active-file="activeFile"        />      </tbody>    </table> diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue index 0d0c34ec741..da0714c368c 100644 --- a/app/assets/javascripts/repo/components/repo_tab.vue +++ b/app/assets/javascripts/repo/components/repo_tab.vue @@ -1,7 +1,7 @@  <script> -import Store from '../stores/repo_store'; +import { mapActions } from 'vuex'; -const RepoTab = { +export default {    props: {      tab: {        type: Object, @@ -11,53 +11,52 @@ const RepoTab = {    computed: {      closeLabel() { -      if (this.tab.changed) { +      if (this.tab.changed || this.tab.tempFile) {          return `${this.tab.name} changed`;        }        return `Close ${this.tab.name}`;      },      changedClass() {        const tabChangedObj = { -        'fa-times close-icon': !this.tab.changed, -        'fa-circle unsaved-icon': this.tab.changed, +        'fa-times close-icon': !this.tab.changed && !this.tab.tempFile, +        'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,        };        return tabChangedObj;      },    },    methods: { -    tabClicked: Store.setActiveFiles, - -    closeTab(file) { -      if (file.changed) return; -      this.$emit('tabclosed', file); -    }, +    ...mapActions([ +      'setFileActive', +      'closeFile', +    ]),    },  }; - -export default RepoTab;  </script>  <template> -<li @click="tabClicked(tab)"> -  <a -    href="#0" -    class="close" -    @click.stop.prevent="closeTab(tab)" -    :aria-label="closeLabel"> -    <i -      class="fa" -      :class="changedClass" -      aria-hidden="true"> -    </i> -  </a> +  <li +    :class="{ active : tab.active }" +    @click="setFileActive(tab)" +  > +    <button +      type="button" +      class="close-btn" +      @click.stop.prevent="closeFile({ file: tab })" +      :aria-label="closeLabel"> +      <i +        class="fa" +        :class="changedClass" +        aria-hidden="true"> +      </i> +    </button> -  <a -    href="#" -    class="repo-tab" -    :title="tab.url" -    @click.prevent="tabClicked(tab)"> -    {{tab.name}} -  </a> -</li> +    <a +      href="#" +      class="repo-tab" +      :title="tab.url" +      @click.prevent.stop="setFileActive(tab)"> +      {{tab.name}} +    </a> +  </li>  </template> diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue index 9c5bfc5d0cf..59beae53e8d 100644 --- a/app/assets/javascripts/repo/components/repo_tabs.vue +++ b/app/assets/javascripts/repo/components/repo_tabs.vue @@ -1,36 +1,29 @@  <script> -import Store from '../stores/repo_store'; -import RepoTab from './repo_tab.vue'; -import RepoMixin from '../mixins/repo_mixin'; +  import { mapState } from 'vuex'; +  import RepoTab from './repo_tab.vue'; -const RepoTabs = { -  mixins: [RepoMixin], - -  components: { -    'repo-tab': RepoTab, -  }, - -  data: () => Store, - -  methods: { -    tabClosed(file) { -      Store.removeFromOpenedFiles(file); +  export default { +    components: { +      'repo-tab': RepoTab,      }, -  }, -}; - -export default RepoTabs; +    computed: { +      ...mapState([ +        'openFiles', +      ]), +    }, +  };  </script>  <template> -<ul id="tabs"> -  <repo-tab -    v-for="tab in openedFiles" -    :key="tab.id" -    :tab="tab" -    :class="{'active' : tab.active}" -    @tabclosed="tabClosed" -  /> -  <li class="tabs-divider" /> -</ul> +  <ul +    id="tabs" +    class="list-unstyled" +  > +    <repo-tab +      v-for="tab in openFiles" +      :key="tab.id" +      :tab="tab" +    /> +    <li class="tabs-divider" /> +  </ul>  </template> diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js deleted file mode 100644 index f8729bbf585..00000000000 --- a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js +++ /dev/null @@ -1,25 +0,0 @@ -/* global monaco */ -import RepoEditor from '../components/repo_editor.vue'; -import Store from '../stores/repo_store'; -import Helper from '../helpers/repo_helper'; -import monacoLoader from '../monaco_loader'; - -function repoEditorLoader() { -  Store.monacoLoading = true; -  return new Promise((resolve, reject) => { -    monacoLoader(['vs/editor/editor.main'], () => { -      Helper.monaco = monaco; -      Store.monacoLoading = false; -      resolve(RepoEditor); -    }, () => { -      Store.monacoLoading = false; -      reject(); -    }); -  }); -} - -const MonacoLoaderHelper = { -  repoEditorLoader, -}; - -export default MonacoLoaderHelper; diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js deleted file mode 100644 index 7483f8bc305..00000000000 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ /dev/null @@ -1,279 +0,0 @@ -/* global Flash */ -import Service from '../services/repo_service'; -import Store from '../stores/repo_store'; -import '../../flash'; - -const RepoHelper = { -  monacoInstance: null, - -  getDefaultActiveFile() { -    return { -      active: true, -      binary: false, -      extension: '', -      html: '', -      mime_type: '', -      name: '', -      plain: '', -      size: 0, -      url: '', -      raw: false, -      newContent: '', -      changed: false, -      loading: false, -    }; -  }, - -  key: '', - -  isTree(data) { -    return Object.hasOwnProperty.call(data, 'blobs'); -  }, - -  Time: window.performance -  && window.performance.now -  ? window.performance -  : Date, - -  getFileExtension(fileName) { -    return fileName.split('.').pop(); -  }, - -  getLanguageIDForFile(file, langs) { -    const ext = RepoHelper.getFileExtension(file.name); -    const foundLang = RepoHelper.findLanguage(ext, langs); - -    return foundLang ? foundLang.id : 'plaintext'; -  }, - -  setMonacoModelFromLanguage() { -    RepoHelper.monacoInstance.setModel(null); -    const languages = RepoHelper.monaco.languages.getLanguages(); -    const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages); -    const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID); -    RepoHelper.monacoInstance.setModel(newModel); -  }, - -  findLanguage(ext, langs) { -    return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1); -  }, - -  setDirectoryOpen(tree, title) { -    const file = tree; -    if (!file) return undefined; - -    file.opened = true; -    file.icon = 'fa-folder-open'; -    RepoHelper.updateHistoryEntry(file.url, title); -    return file; -  }, - -  isRenderable() { -    const okExts = ['md', 'svg']; -    return okExts.indexOf(Store.activeFile.extension) > -1; -  }, - -  setBinaryDataAsBase64(file) { -    Service.getBase64Content(file.raw_path) -    .then((response) => { -      Store.blobRaw = response; -      file.base64 = response; // eslint-disable-line no-param-reassign -    }) -    .catch(RepoHelper.loadingError); -  }, - -  // when you open a directory you need to put the directory files under -  // the directory... This will merge the list of the current directory and the new list. -  getNewMergedList(inDirectory, currentList, newList) { -    const newListSorted = newList.sort(this.compareFilesCaseInsensitive); -    if (!inDirectory) return newListSorted; -    const indexOfFile = currentList.findIndex(file => file.url === inDirectory.url); -    if (!indexOfFile) return newListSorted; -    return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile); -  }, - -  // within the get new merged list this does the merging of the current list of files -  // and the new list of files. The files are never "in" another directory they just -  // appear like they are because of the margin. -  mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) { -    newList.reverse().forEach((newFile) => { -      const fileIndex = indexOfFile + 1; -      const file = newFile; -      file.level = inDirectory.level + 1; -      oldList.splice(fileIndex, 0, file); -    }); - -    return oldList; -  }, - -  compareFilesCaseInsensitive(a, b) { -    const aName = a.name.toLowerCase(); -    const bName = b.name.toLowerCase(); -    if (a.level > 0) return 0; -    if (aName < bName) { return -1; } -    if (aName > bName) { return 1; } -    return 0; -  }, - -  isRoot(url) { -    // the url we are requesting -> split by the project URL. Grab the right side. -    const isRoot = !!url.split(Store.projectUrl)[1] -    // remove the first "/" -    .slice(1) -    // split this by "/" -    .split('/') -    // remove the first two items of the array... usually /tree/master. -    .slice(2) -    // we want to know the length of the array. -    // If greater than 0 not root. -    .length; -    return isRoot; -  }, - -  getContent(treeOrFile) { -    let file = treeOrFile; -    return Service.getContent() -    .then((response) => { -      const data = response.data; -      if (response.headers && response.headers['page-title']) data.pageTitle = response.headers['page-title']; - -      Store.isTree = RepoHelper.isTree(data); -      if (!Store.isTree) { -        if (!file) file = data; -        Store.binary = data.binary; - -        if (data.binary) { -          // file might be undefined -          RepoHelper.setBinaryDataAsBase64(data); -          Store.setViewToPreview(); -        } else if (!Store.isPreviewView()) { -          if (!data.render_error) { -            Service.getRaw(data.raw_path) -            .then((rawResponse) => { -              Store.blobRaw = rawResponse.data; -              data.plain = rawResponse.data; -              RepoHelper.setFile(data, file); -            }).catch(RepoHelper.loadingError); -          } -        } - -        if (Store.isPreviewView()) { -          RepoHelper.setFile(data, file); -        } - -        // if the file tree is empty -        if (Store.files.length === 0) { -          const parentURL = Service.blobURLtoParentTree(Service.url); -          Service.url = parentURL; -          RepoHelper.getContent(); -        } -      } else { -        // it's a tree -        if (!file) Store.isRoot = RepoHelper.isRoot(Service.url); -        file = RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name); -        const newDirectory = RepoHelper.dataToListOfFiles(data); -        Store.addFilesToDirectory(file, Store.files, newDirectory); -        Store.prevURL = Service.blobURLtoParentTree(Service.url); -      } -    }).catch(RepoHelper.loadingError); -  }, - -  setFile(data, file) { -    const newFile = data; -    newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh. - -    if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') { -      newFile.tooLarge = true; -    } -    newFile.newContent = ''; - -    Store.addToOpenedFiles(newFile); -    Store.setActiveFiles(newFile); -  }, - -  serializeBlob(blob) { -    const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob); -    simpleBlob.lastCommitMessage = blob.last_commit.message; -    simpleBlob.lastCommitUpdate = blob.last_commit.committed_date; -    simpleBlob.loading = false; - -    return simpleBlob; -  }, - -  serializeTree(tree) { -    return RepoHelper.serializeRepoEntity('tree', tree); -  }, - -  serializeSubmodule(submodule) { -    return RepoHelper.serializeRepoEntity('submodule', submodule); -  }, - -  serializeRepoEntity(type, entity) { -    const { url, name, icon, last_commit } = entity; -    const returnObj = { -      type, -      name, -      url, -      icon: `fa-${icon}`, -      level: 0, -      loading: false, -    }; - -    if (entity.last_commit) { -      returnObj.lastCommitUrl = `${Store.projectUrl}/commit/${last_commit.id}`; -    } else { -      returnObj.lastCommitUrl = ''; -    } -    return returnObj; -  }, - -  scrollTabsRight() { -    // wait for the transition. 0.1 seconds. -    setTimeout(() => { -      const tabs = document.getElementById('tabs'); -      if (!tabs) return; -      tabs.scrollLeft = tabs.scrollWidth; -    }, 200); -  }, - -  dataToListOfFiles(data) { -    const { blobs, trees, submodules } = data; -    return [ -      ...blobs.map(blob => RepoHelper.serializeBlob(blob)), -      ...trees.map(tree => RepoHelper.serializeTree(tree)), -      ...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)), -    ]; -  }, - -  genKey() { -    return RepoHelper.Time.now().toFixed(3); -  }, - -  updateHistoryEntry(url, title) { -    const history = window.history; - -    RepoHelper.key = RepoHelper.genKey(); - -    if (document.location.pathname !== url) { -      history.pushState({ key: RepoHelper.key }, '', url); -    } - -    if (title) { -      document.title = title; -    } -  }, - -  findOpenedFileFromActive() { -    return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url); -  }, - -  getFileFromPath(path) { -    return Store.openedFiles.find(file => file.url === path); -  }, - -  loadingError() { -    Flash('Unable to load this content at this time.'); -  }, -}; - -export default RepoHelper; diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 7d0123e3d3a..b6801af7fcb 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -1,46 +1,50 @@ -import $ from 'jquery';  import Vue from 'vue'; -import Service from './services/repo_service'; -import Store from './stores/repo_store'; +import { mapActions } from 'vuex'; +import { convertPermissionToBoolean } from '../lib/utils/common_utils';  import Repo from './components/repo.vue';  import RepoEditButton from './components/repo_edit_button.vue'; +import newBranchForm from './components/new_branch_form.vue'; +import newDropdown from './components/new_dropdown/index.vue'; +import store from './stores';  import Translate from '../vue_shared/translate'; -function initDropdowns() { -  $('.js-tree-ref-target-holder').hide(); -} - -function addEventsForNonVueEls() { -  window.onbeforeunload = function confirmUnload(e) { -    const hasChanged = Store.openedFiles -      .some(file => file.changed); -    if (!hasChanged) return undefined; -    const event = e || window.event; -    if (event) event.returnValue = 'Are you sure you want to lose unsaved changes?'; -    // For Safari -    return 'Are you sure you want to lose unsaved changes?'; -  }; -} - -function setInitialStore(data) { -  Store.service = Service; -  Store.service.url = data.url; -  Store.service.refsUrl = data.refsUrl; -  Store.projectId = data.projectId; -  Store.projectName = data.projectName; -  Store.projectUrl = data.projectUrl; -  Store.canCommit = data.canCommit; -  Store.onTopOfBranch = data.onTopOfBranch; -  Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); -  Store.checkIsCommitable(); -} -  function initRepo(el) { +  if (!el) return null; +    return new Vue({      el, +    store,      components: {        repo: Repo,      }, +    methods: { +      ...mapActions([ +        'setInitialData', +      ]), +    }, +    created() { +      const data = el.dataset; + +      this.setInitialData({ +        project: { +          id: data.projectId, +          name: data.projectName, +          url: data.projectUrl, +        }, +        endpoints: { +          rootEndpoint: data.url, +          newMergeRequestUrl: data.newMergeRequestUrl, +          rootUrl: data.rootUrl, +        }, +        canCommit: convertPermissionToBoolean(data.canCommit), +        onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch), +        currentRef: data.ref, +        path: data.currentPath, +        currentBranch: data.currentBranch, +        isRoot: convertPermissionToBoolean(data.root), +        isInitialRoot: convertPermissionToBoolean(data.root), +      }); +    },      render(createElement) {        return createElement('repo');      }, @@ -50,25 +54,53 @@ function initRepo(el) {  function initRepoEditButton(el) {    return new Vue({      el, +    store,      components: {        repoEditButton: RepoEditButton,      }, +    render(createElement) { +      return createElement('repo-edit-button'); +    },    });  } -function initRepoBundle() { -  const repo = document.getElementById('repo'); -  const editButton = document.querySelector('.editable-mode'); -  setInitialStore(repo.dataset); -  addEventsForNonVueEls(); -  initDropdowns(); +function initNewDropdown(el) { +  return new Vue({ +    el, +    store, +    components: { +      newDropdown, +    }, +    render(createElement) { +      return createElement('new-dropdown'); +    }, +  }); +} + +function initNewBranchForm() { +  const el = document.querySelector('.js-new-branch-dropdown'); -  Vue.use(Translate); +  if (!el) return null; -  initRepo(repo); -  initRepoEditButton(editButton); +  return new Vue({ +    el, +    components: { +      newBranchForm, +    }, +    store, +    render(createElement) { +      return createElement('new-branch-form'); +    }, +  });  } -$(initRepoBundle); +const repo = document.getElementById('repo'); +const editButton = document.querySelector('.editable-mode'); +const newDropdownHolder = document.querySelector('.js-new-dropdown'); + +Vue.use(Translate); -export default initRepoBundle; +initRepo(repo); +initRepoEditButton(editButton); +initNewBranchForm(); +initNewDropdown(newDropdownHolder); diff --git a/app/assets/javascripts/repo/mixins/repo_mixin.js b/app/assets/javascripts/repo/mixins/repo_mixin.js deleted file mode 100644 index c8e8238a0d3..00000000000 --- a/app/assets/javascripts/repo/mixins/repo_mixin.js +++ /dev/null @@ -1,17 +0,0 @@ -import Store from '../stores/repo_store'; - -const RepoMixin = { -  computed: { -    isMini() { -      return !!Store.openedFiles.length; -    }, - -    changedFiles() { -      const changedFileList = this.openedFiles -      .filter(file => file.changed); -      return changedFileList; -    }, -  }, -}; - -export default RepoMixin; diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/repo/services/index.js new file mode 100644 index 00000000000..2fb45dcb03c --- /dev/null +++ b/app/assets/javascripts/repo/services/index.js @@ -0,0 +1,40 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import Api from '../../api'; + +Vue.use(VueResource); + +export default { +  getTreeData(endpoint) { +    return Vue.http.get(endpoint, { params: { format: 'json' } }); +  }, +  getFileData(endpoint) { +    return Vue.http.get(endpoint, { params: { format: 'json' } }); +  }, +  getRawFileData(file) { +    if (file.tempFile) { +      return Promise.resolve(file.content); +    } + +    return Vue.http.get(file.rawPath, { params: { format: 'json' } }) +      .then(res => res.text()); +  }, +  getBranchData(projectId, currentBranch) { +    return Api.branchSingle(projectId, currentBranch); +  }, +  createBranch(projectId, payload) { +    const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId); + +    return Vue.http.post(url, payload); +  }, +  commit(projectId, payload) { +    return Api.commitMultiple(projectId, payload); +  }, +  getTreeLastCommit(endpoint) { +    return Vue.http.get(endpoint, { +      params: { +        format: 'json', +      }, +    }); +  }, +}; diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js deleted file mode 100644 index af83497fa39..00000000000 --- a/app/assets/javascripts/repo/services/repo_service.js +++ /dev/null @@ -1,82 +0,0 @@ -/* global Flash */ -import axios from 'axios'; -import Store from '../stores/repo_store'; -import Api from '../../api'; -import Helper from '../helpers/repo_helper'; - -const RepoService = { -  url: '', -  options: { -    params: { -      format: 'json', -    }, -  }, -  richExtensionRegExp: /md/, - -  getRaw(url) { -    return axios.get(url, { -      // Stop Axios from parsing a JSON file into a JS object -      transformResponse: [res => res], -    }); -  }, - -  buildParams(url = this.url) { -    // shallow clone object without reference -    const params = Object.assign({}, this.options.params); - -    if (this.urlIsRichBlob(url)) params.viewer = 'rich'; - -    return params; -  }, - -  urlIsRichBlob(url = this.url) { -    const extension = Helper.getFileExtension(url); - -    return this.richExtensionRegExp.test(extension); -  }, - -  getContent(url = this.url) { -    const params = this.buildParams(url); - -    return axios.get(url, { -      params, -    }); -  }, - -  getBase64Content(url = this.url) { -    const request = axios.get(url, { -      responseType: 'arraybuffer', -    }); - -    return request.then(response => this.bufferToBase64(response.data)); -  }, - -  bufferToBase64(data) { -    return new Buffer(data, 'binary').toString('base64'); -  }, - -  blobURLtoParentTree(url) { -    const urlArray = url.split('/'); -    urlArray.pop(); -    const blobIndex = urlArray.lastIndexOf('blob'); - -    if (blobIndex > -1) urlArray[blobIndex] = 'tree'; - -    return urlArray.join('/'); -  }, - -  commitFiles(payload) { -    return Api.commitMultiple(Store.projectId, payload) -      .then(this.commitFlash); -  }, - -  commitFlash(data) { -    if (data.short_id && data.stats) { -      window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); -    } else { -      window.Flash(data.message); -    } -  }, -}; - -export default RepoService; diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js new file mode 100644 index 00000000000..be290c268b1 --- /dev/null +++ b/app/assets/javascripts/repo/stores/actions.js @@ -0,0 +1,145 @@ +import Vue from 'vue'; +import flash from '../../flash'; +import service from '../services'; +import * as types from './mutation_types'; + +export const redirectToUrl = url => gl.utils.visitUrl(url); + +export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); + +export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false); + +export const discardAllChanges = ({ commit, getters, dispatch }) => { +  const changedFiles = getters.changedFiles; + +  changedFiles.forEach((file) => { +    commit(types.DISCARD_FILE_CHANGES, file); + +    if (file.tempFile) { +      dispatch('closeFile', { file, force: true }); +    } +  }); +}; + +export const closeAllFiles = ({ state, dispatch }) => { +  state.openFiles.forEach(file => dispatch('closeFile', { file })); +}; + +export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => { +  const changedFiles = getters.changedFiles; + +  if (changedFiles.length && !force) { +    commit(types.TOGGLE_DISCARD_POPUP, true); +  } else { +    commit(types.TOGGLE_EDIT_MODE); +    commit(types.TOGGLE_DISCARD_POPUP, false); +    dispatch('toggleBlobView'); + +    if (!state.editMode) { +      dispatch('discardAllChanges'); +    } +  } +}; + +export const toggleBlobView = ({ commit, state }) => { +  if (state.editMode) { +    commit(types.SET_EDIT_MODE); +  } else { +    commit(types.SET_PREVIEW_MODE); +  } +}; + +export const checkCommitStatus = ({ state }) => service.getBranchData( +  state.project.id, +  state.currentBranch, +) +  .then((data) => { +    const { id } = data.commit; + +    if (state.currentRef !== id) { +      return true; +    } + +    return false; +  }) +  .catch(() => flash('Error checking branch data. Please try again.')); + +export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) => +  service.commit(state.project.id, payload) +  .then((data) => { +    const { branch } = payload; +    if (!data.short_id) { +      flash(data.message); +      return; +    } + +    const lastCommit = { +      commit_path: `${state.project.url}/commit/${data.id}`, +      commit: { +        message: data.message, +        authored_date: data.committed_date, +      }, +    }; + +    flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); + +    if (newMr) { +      redirectToUrl(`${state.endpoints.newMergeRequestUrl}${branch}`); +    } else { +      commit(types.SET_COMMIT_REF, data.id); + +      getters.changedFiles.forEach((entry) => { +        commit(types.SET_LAST_COMMIT_DATA, { +          entry, +          lastCommit, +        }); +      }); + +      dispatch('discardAllChanges'); +      dispatch('closeAllFiles'); +      dispatch('toggleEditMode'); + +      window.scrollTo(0, 0); +    } +  }) +  .catch(() => flash('Error committing changes. Please try again.')); + +export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => { +  if (type === 'tree') { +    dispatch('createTempTree', name); +  } else if (type === 'blob') { +    dispatch('createTempFile', { +      tree: state, +      name, +      base64, +      content, +    }); +  } +}; + +export const popHistoryState = ({ state, dispatch, getters }) => { +  const treeList = getters.treeList; +  const tree = treeList.find(file => file.url === state.previousUrl); + +  if (!tree) return; + +  if (tree.type === 'tree') { +    dispatch('toggleTreeOpen', { endpoint: tree.url, tree }); +  } +}; + +export const scrollToTab = () => { +  Vue.nextTick(() => { +    const tabs = document.getElementById('tabs'); + +    if (tabs) { +      const tabEl = tabs.querySelector('.active .repo-tab'); + +      tabEl.focus(); +    } +  }); +}; + +export * from './actions/tree'; +export * from './actions/file'; +export * from './actions/branch'; diff --git a/app/assets/javascripts/repo/stores/actions/branch.js b/app/assets/javascripts/repo/stores/actions/branch.js new file mode 100644 index 00000000000..b81a70dfd1e --- /dev/null +++ b/app/assets/javascripts/repo/stores/actions/branch.js @@ -0,0 +1,20 @@ +import service from '../../services'; +import * as types from '../mutation_types'; +import { pushState } from '../utils'; + +// eslint-disable-next-line import/prefer-default-export +export const createNewBranch = ({ rootState, commit }, branch) => service.createBranch( +  rootState.project.id, +  { +    branch, +    ref: rootState.currentBranch, +  }, +).then(res => res.json()) +.then((data) => { +  const branchName = data.name; +  const url = location.href.replace(rootState.currentBranch, branchName); + +  pushState(url); + +  commit(types.SET_CURRENT_BRANCH, branchName); +}); diff --git a/app/assets/javascripts/repo/stores/actions/file.js b/app/assets/javascripts/repo/stores/actions/file.js new file mode 100644 index 00000000000..5bae4fa826a --- /dev/null +++ b/app/assets/javascripts/repo/stores/actions/file.js @@ -0,0 +1,110 @@ +import { normalizeHeaders } from '../../../lib/utils/common_utils'; +import flash from '../../../flash'; +import service from '../../services'; +import * as types from '../mutation_types'; +import { +  findEntry, +  pushState, +  setPageTitle, +  createTemp, +  findIndexOfFile, +} from '../utils'; + +export const closeFile = ({ commit, state, dispatch }, { file, force = false }) => { +  if ((file.changed || file.tempFile) && !force) return; + +  const indexOfClosedFile = findIndexOfFile(state.openFiles, file); +  const fileWasActive = file.active; + +  commit(types.TOGGLE_FILE_OPEN, file); +  commit(types.SET_FILE_ACTIVE, { file, active: false }); + +  if (state.openFiles.length > 0 && fileWasActive) { +    const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1; +    const nextFileToOpen = state.openFiles[nextIndexToOpen]; + +    dispatch('setFileActive', nextFileToOpen); +  } else if (!state.openFiles.length) { +    pushState(file.parentTreeUrl); +  } + +  dispatch('getLastCommitData'); +}; + +export const setFileActive = ({ commit, state, getters, dispatch }, file) => { +  const currentActiveFile = getters.activeFile; + +  if (file.active) return; + +  if (currentActiveFile) { +    commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false }); +  } + +  commit(types.SET_FILE_ACTIVE, { file, active: true }); +  dispatch('scrollToTab'); + +  // reset hash for line highlighting +  location.hash = ''; +}; + +export const getFileData = ({ state, commit, dispatch }, file) => { +  commit(types.TOGGLE_LOADING, file); + +  service.getFileData(file.url) +    .then((res) => { +      const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); + +      setPageTitle(pageTitle); + +      return res.json(); +    }) +    .then((data) => { +      commit(types.SET_FILE_DATA, { data, file }); +      commit(types.TOGGLE_FILE_OPEN, file); +      dispatch('setFileActive', file); +      commit(types.TOGGLE_LOADING, file); + +      pushState(file.url); +    }) +    .catch(() => { +      commit(types.TOGGLE_LOADING, file); +      flash('Error loading file data. Please try again.'); +    }); +}; + +export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file) +  .then((raw) => { +    commit(types.SET_FILE_RAW_DATA, { file, raw }); +  }) +  .catch(() => flash('Error loading file content. Please try again.')); + +export const changeFileContent = ({ commit }, { file, content }) => { +  commit(types.UPDATE_FILE_CONTENT, { file, content }); +}; + +export const createTempFile = ({ state, commit, dispatch }, { tree, name, content = '', base64 = '' }) => { +  const file = createTemp({ +    name: name.replace(`${state.path}/`, ''), +    path: tree.path, +    type: 'blob', +    level: tree.level !== undefined ? tree.level + 1 : 0, +    changed: true, +    content, +    base64, +  }); + +  if (findEntry(tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`); + +  commit(types.CREATE_TMP_FILE, { +    parent: tree, +    file, +  }); +  commit(types.TOGGLE_FILE_OPEN, file); +  dispatch('setFileActive', file); + +  if (!state.editMode && !file.base64) { +    dispatch('toggleEditMode', true); +  } + +  return Promise.resolve(file); +}; diff --git a/app/assets/javascripts/repo/stores/actions/tree.js b/app/assets/javascripts/repo/stores/actions/tree.js new file mode 100644 index 00000000000..aa830e946a2 --- /dev/null +++ b/app/assets/javascripts/repo/stores/actions/tree.js @@ -0,0 +1,162 @@ +import { normalizeHeaders } from '../../../lib/utils/common_utils'; +import flash from '../../../flash'; +import service from '../../services'; +import * as types from '../mutation_types'; +import { +  pushState, +  setPageTitle, +  findEntry, +  createTemp, +  createOrMergeEntry, +} from '../utils'; + +export const getTreeData = ( +  { commit, state, dispatch }, +  { endpoint = state.endpoints.rootEndpoint, tree = state } = {}, +) => { +  commit(types.TOGGLE_LOADING, tree); + +  service.getTreeData(endpoint) +    .then((res) => { +      const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); + +      setPageTitle(pageTitle); + +      return res.json(); +    }) +    .then((data) => { +      const prevLastCommitPath = tree.lastCommitPath; +      if (!state.isInitialRoot) { +        commit(types.SET_ROOT, data.path === '/'); +      } + +      dispatch('updateDirectoryData', { data, tree }); +      commit(types.SET_PARENT_TREE_URL, data.parent_tree_url); +      commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path }); +      commit(types.TOGGLE_LOADING, tree); + +      if (prevLastCommitPath !== null) { +        dispatch('getLastCommitData', tree); +      } + +      pushState(endpoint); +    }) +    .catch(() => { +      flash('Error loading tree data. Please try again.'); +      commit(types.TOGGLE_LOADING, tree); +    }); +}; + +export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => { +  if (tree.opened) { +    // send empty data to clear the tree +    const data = { trees: [], blobs: [], submodules: [] }; + +    pushState(tree.parentTreeUrl); + +    commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl); +    dispatch('updateDirectoryData', { data, tree }); +  } else { +    commit(types.SET_PREVIOUS_URL, endpoint); +    dispatch('getTreeData', { endpoint, tree }); +  } + +  commit(types.TOGGLE_TREE_OPEN, tree); +}; + +export const clickedTreeRow = ({ commit, dispatch }, row) => { +  if (row.type === 'tree') { +    dispatch('toggleTreeOpen', { +      endpoint: row.url, +      tree: row, +    }); +  } else if (row.type === 'submodule') { +    commit(types.TOGGLE_LOADING, row); + +    gl.utils.visitUrl(row.url); +  } else if (row.type === 'blob' && row.opened) { +    dispatch('setFileActive', row); +  } else { +    dispatch('getFileData', row); +  } +}; + +export const createTempTree = ({ state, commit, dispatch }, name) => { +  let tree = state; +  const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/'); + +  dirNames.forEach((dirName) => { +    const foundEntry = findEntry(tree, 'tree', dirName); + +    if (!foundEntry) { +      const tmpEntry = createTemp({ +        name: dirName, +        path: tree.path, +        type: 'tree', +        level: tree.level !== undefined ? tree.level + 1 : 0, +      }); + +      commit(types.CREATE_TMP_TREE, { +        parent: tree, +        tmpEntry, +      }); +      commit(types.TOGGLE_TREE_OPEN, tmpEntry); + +      tree = tmpEntry; +    } else { +      tree = foundEntry; +    } +  }); + +  if (tree.tempFile) { +    dispatch('createTempFile', { +      tree, +      name: '.gitkeep', +    }); +  } +}; + +export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { +  if (tree.lastCommitPath === null || getters.isCollapsed) return; + +  service.getTreeLastCommit(tree.lastCommitPath) +    .then((res) => { +      const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null; + +      commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath }); + +      return res.json(); +    }) +    .then((data) => { +      data.forEach((lastCommit) => { +        const entry = findEntry(tree, lastCommit.type, lastCommit.file_name); + +        if (entry) { +          commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit }); +        } +      }); + +      dispatch('getLastCommitData', tree); +    }) +    .catch(() => flash('Error fetching log data.')); +}; + +export const updateDirectoryData = ({ commit, state }, { data, tree }) => { +  const level = tree.level !== undefined ? tree.level + 1 : 0; +  const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl; +  const createEntry = (entry, type) => createOrMergeEntry({ +    tree, +    entry, +    level, +    type, +    parentTreeUrl, +  }); + +  const formattedData = [ +    ...data.trees.map(t => createEntry(t, 'tree')), +    ...data.submodules.map(m => createEntry(m, 'submodule')), +    ...data.blobs.map(b => createEntry(b, 'blob')), +  ]; + +  commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData }); +}; diff --git a/app/assets/javascripts/repo/stores/getters.js b/app/assets/javascripts/repo/stores/getters.js new file mode 100644 index 00000000000..1ed05ac6e35 --- /dev/null +++ b/app/assets/javascripts/repo/stores/getters.js @@ -0,0 +1,36 @@ +import _ from 'underscore'; + +/* +  Takes the multi-dimensional tree and returns a flattened array. +  This allows for the table to recursively render the table rows but keeps the data +  structure nested to make it easier to add new files/directories. +*/ +export const treeList = (state) => { +  const mapTree = arr => (!arr.tree.length ? [] : _.map(arr.tree, a => [a, mapTree(a)])); + +  return _.chain(state.tree) +    .map(arr => [arr, mapTree(arr)]) +    .flatten() +    .value(); +}; + +export const changedFiles = state => state.openFiles.filter(file => file.changed); + +export const activeFile = state => state.openFiles.find(file => file.active); + +export const activeFileExtension = (state) => { +  const file = activeFile(state); +  return file ? `.${file.path.split('.').pop()}` : ''; +}; + +export const isCollapsed = state => !!state.openFiles.length; + +export const canEditFile = (state) => { +  const currentActiveFile = activeFile(state); +  const openedFiles = state.openFiles; + +  return state.canCommit && +    state.onTopOfBranch && +    openedFiles.length && +    (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary); +}; diff --git a/app/assets/javascripts/repo/stores/index.js b/app/assets/javascripts/repo/stores/index.js new file mode 100644 index 00000000000..6ac9bfd8189 --- /dev/null +++ b/app/assets/javascripts/repo/stores/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import state from './state'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default new Vuex.Store({ +  state: state(), +  actions, +  mutations, +  getters, +}); diff --git a/app/assets/javascripts/repo/stores/mutation_types.js b/app/assets/javascripts/repo/stores/mutation_types.js new file mode 100644 index 00000000000..bc3390f1506 --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutation_types.js @@ -0,0 +1,30 @@ +export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; +export const TOGGLE_LOADING = 'TOGGLE_LOADING'; +export const SET_COMMIT_REF = 'SET_COMMIT_REF'; +export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL'; +export const SET_ROOT = 'SET_ROOT'; +export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL'; +export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA'; + +// Tree mutation types +export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; +export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN'; +export const CREATE_TMP_TREE = 'CREATE_TMP_TREE'; +export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL'; + +// File mutation types +export const SET_FILE_DATA = 'SET_FILE_DATA'; +export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN'; +export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE'; +export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA'; +export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; +export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES'; +export const CREATE_TMP_FILE = 'CREATE_TMP_FILE'; + +// Viewer mutation types +export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE'; +export const SET_EDIT_MODE = 'SET_EDIT_MODE'; +export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE'; +export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP'; + +export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; diff --git a/app/assets/javascripts/repo/stores/mutations.js b/app/assets/javascripts/repo/stores/mutations.js new file mode 100644 index 00000000000..ae2ba5bedf7 --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutations.js @@ -0,0 +1,61 @@ +import * as types from './mutation_types'; +import fileMutations from './mutations/file'; +import treeMutations from './mutations/tree'; +import branchMutations from './mutations/branch'; + +export default { +  [types.SET_INITIAL_DATA](state, data) { +    Object.assign(state, data); +  }, +  [types.SET_PREVIEW_MODE](state) { +    Object.assign(state, { +      currentBlobView: 'repo-preview', +    }); +  }, +  [types.SET_EDIT_MODE](state) { +    Object.assign(state, { +      currentBlobView: 'repo-editor', +    }); +  }, +  [types.TOGGLE_LOADING](state, entry) { +    Object.assign(entry, { +      loading: !entry.loading, +    }); +  }, +  [types.TOGGLE_EDIT_MODE](state) { +    Object.assign(state, { +      editMode: !state.editMode, +    }); +  }, +  [types.TOGGLE_DISCARD_POPUP](state, discardPopupOpen) { +    Object.assign(state, { +      discardPopupOpen, +    }); +  }, +  [types.SET_COMMIT_REF](state, ref) { +    Object.assign(state, { +      currentRef: ref, +    }); +  }, +  [types.SET_ROOT](state, isRoot) { +    Object.assign(state, { +      isRoot, +      isInitialRoot: isRoot, +    }); +  }, +  [types.SET_PREVIOUS_URL](state, previousUrl) { +    Object.assign(state, { +      previousUrl, +    }); +  }, +  [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) { +    Object.assign(entry.lastCommit, { +      url: lastCommit.commit_path, +      message: lastCommit.commit.message, +      updatedAt: lastCommit.commit.authored_date, +    }); +  }, +  ...fileMutations, +  ...treeMutations, +  ...branchMutations, +}; diff --git a/app/assets/javascripts/repo/stores/mutations/branch.js b/app/assets/javascripts/repo/stores/mutations/branch.js new file mode 100644 index 00000000000..d8229e8a620 --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutations/branch.js @@ -0,0 +1,9 @@ +import * as types from '../mutation_types'; + +export default { +  [types.SET_CURRENT_BRANCH](state, currentBranch) { +    Object.assign(state, { +      currentBranch, +    }); +  }, +}; diff --git a/app/assets/javascripts/repo/stores/mutations/file.js b/app/assets/javascripts/repo/stores/mutations/file.js new file mode 100644 index 00000000000..f9ba80b9dc2 --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutations/file.js @@ -0,0 +1,54 @@ +import * as types from '../mutation_types'; +import { findIndexOfFile } from '../utils'; + +export default { +  [types.SET_FILE_ACTIVE](state, { file, active }) { +    Object.assign(file, { +      active, +    }); +  }, +  [types.TOGGLE_FILE_OPEN](state, file) { +    Object.assign(file, { +      opened: !file.opened, +    }); + +    if (file.opened) { +      state.openFiles.push(file); +    } else { +      state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1); +    } +  }, +  [types.SET_FILE_DATA](state, { data, file }) { +    Object.assign(file, { +      blamePath: data.blame_path, +      commitsPath: data.commits_path, +      permalink: data.permalink, +      rawPath: data.raw_path, +      binary: data.binary, +      html: data.html, +      renderError: data.render_error, +    }); +  }, +  [types.SET_FILE_RAW_DATA](state, { file, raw }) { +    Object.assign(file, { +      raw, +    }); +  }, +  [types.UPDATE_FILE_CONTENT](state, { file, content }) { +    const changed = content !== file.raw; + +    Object.assign(file, { +      content, +      changed, +    }); +  }, +  [types.DISCARD_FILE_CHANGES](state, file) { +    Object.assign(file, { +      content: '', +      changed: false, +    }); +  }, +  [types.CREATE_TMP_FILE](state, { file, parent }) { +    parent.tree.push(file); +  }, +}; diff --git a/app/assets/javascripts/repo/stores/mutations/tree.js b/app/assets/javascripts/repo/stores/mutations/tree.js new file mode 100644 index 00000000000..130221c9fda --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutations/tree.js @@ -0,0 +1,27 @@ +import * as types from '../mutation_types'; + +export default { +  [types.TOGGLE_TREE_OPEN](state, tree) { +    Object.assign(tree, { +      opened: !tree.opened, +    }); +  }, +  [types.SET_DIRECTORY_DATA](state, { data, tree }) { +    Object.assign(tree, { +      tree: data, +    }); +  }, +  [types.SET_PARENT_TREE_URL](state, url) { +    Object.assign(state, { +      parentTreeUrl: url, +    }); +  }, +  [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) { +    Object.assign(tree, { +      lastCommitPath: url, +    }); +  }, +  [types.CREATE_TMP_TREE](state, { parent, tmpEntry }) { +    parent.tree.push(tmpEntry); +  }, +}; diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js deleted file mode 100644 index 93b39cff27e..00000000000 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ /dev/null @@ -1,203 +0,0 @@ -/* global Flash */ -import Helper from '../helpers/repo_helper'; -import Service from '../services/repo_service'; - -const RepoStore = { -  monaco: {}, -  monacoLoading: false, -  service: '', -  canCommit: false, -  onTopOfBranch: false, -  editMode: false, -  isTree: false, -  isRoot: false, -  prevURL: '', -  projectId: '', -  projectName: '', -  projectUrl: '', -  blobRaw: '', -  currentBlobView: 'repo-preview', -  openedFiles: [], -  submitCommitsLoading: false, -  dialog: { -    open: false, -    title: '', -    status: false, -  }, -  activeFile: Helper.getDefaultActiveFile(), -  activeFileIndex: 0, -  activeLine: -1, -  activeFileLabel: 'Raw', -  files: [], -  isCommitable: false, -  binary: false, -  currentBranch: '', -  commitMessage: '', -  binaryTypes: { -    png: false, -    md: false, -    svg: false, -    unknown: false, -  }, -  loading: { -    tree: false, -    blob: false, -  }, - -  resetBinaryTypes() { -    Object.keys(RepoStore.binaryTypes).forEach((key) => { -      RepoStore.binaryTypes[key] = false; -    }); -  }, - -  // mutations -  checkIsCommitable() { -    RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit; -  }, - -  addFilesToDirectory(inDirectory, currentList, newList) { -    RepoStore.files = Helper.getNewMergedList(inDirectory, currentList, newList); -  }, - -  toggleRawPreview() { -    RepoStore.activeFile.raw = !RepoStore.activeFile.raw; -    RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source'; -  }, - -  setActiveFiles(file) { -    if (RepoStore.isActiveFile(file)) return; -    RepoStore.openedFiles = RepoStore.openedFiles -      .map((openedFile, i) => RepoStore.setFileActivity(file, openedFile, i)); - -    RepoStore.setActiveToRaw(); - -    if (file.binary) { -      RepoStore.blobRaw = file.base64; -    } else if (file.newContent || file.plain) { -      RepoStore.blobRaw = file.newContent || file.plain; -    } else { -      Service.getRaw(file.raw_path) -        .then((rawResponse) => { -          RepoStore.blobRaw = rawResponse.data; -          Helper.findOpenedFileFromActive().plain = rawResponse.data; -        }).catch(Helper.loadingError); -    } - -    if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name); -    RepoStore.binary = file.binary; -    RepoStore.setActiveLine(-1); -  }, - -  setFileActivity(file, openedFile, i) { -    const activeFile = openedFile; -    activeFile.active = file.url === activeFile.url; - -    if (activeFile.active) RepoStore.setActiveFile(activeFile, i); - -    return activeFile; -  }, - -  setActiveFile(activeFile, i) { -    RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, activeFile); -    RepoStore.activeFileIndex = i; -  }, - -  setActiveLine(activeLine) { -    if (!isNaN(activeLine)) RepoStore.activeLine = activeLine; -  }, - -  setActiveToRaw() { -    RepoStore.activeFile.raw = false; -    // can't get vue to listen to raw for some reason so RepoStore for now. -    RepoStore.activeFileLabel = 'Display source'; -  }, - -  removeChildFilesOfTree(tree) { -    let foundTree = false; -    const treeToClose = tree; -    let canStopSearching = false; -    RepoStore.files = RepoStore.files.filter((file) => { -      const isItTheTreeWeWant = file.url === treeToClose.url; -      // if it's the next tree -      if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) { -        canStopSearching = true; -        return true; -      } -      if (canStopSearching) return true; - -      if (isItTheTreeWeWant) foundTree = true; - -      if (foundTree) return file.level <= treeToClose.level; -      return true; -    }); - -    treeToClose.opened = false; -    treeToClose.icon = 'fa-folder'; -    return treeToClose; -  }, - -  removeFromOpenedFiles(file) { -    if (file.type === 'tree') return; -    let foundIndex; -    RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => { -      if (openedFile.path === file.path) foundIndex = i; -      return openedFile.path !== file.path; -    }); - -    // now activate the right tab based on what you closed. -    if (RepoStore.openedFiles.length === 0) { -      RepoStore.activeFile = {}; -      return; -    } - -    if (RepoStore.openedFiles.length === 1 || foundIndex === 0) { -      RepoStore.setActiveFiles(RepoStore.openedFiles[0]); -      return; -    } - -    if (foundIndex && foundIndex > 0) { -      RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]); -    } -  }, - -  addToOpenedFiles(file) { -    const openFile = file; - -    const openedFilesAlreadyExists = RepoStore.openedFiles -      .some(openedFile => openedFile.path === openFile.path); - -    if (openedFilesAlreadyExists) return; - -    openFile.changed = false; -    RepoStore.openedFiles.push(openFile); -  }, - -  setActiveFileContents(contents) { -    if (!RepoStore.editMode) return; -    const currentFile = RepoStore.openedFiles[RepoStore.activeFileIndex]; -    RepoStore.activeFile.newContent = contents; -    RepoStore.activeFile.changed = RepoStore.activeFile.plain !== RepoStore.activeFile.newContent; -    currentFile.changed = RepoStore.activeFile.changed; -    currentFile.newContent = contents; -  }, - -  toggleBlobView() { -    RepoStore.currentBlobView = RepoStore.isPreviewView() ? 'repo-editor' : 'repo-preview'; -  }, - -  setViewToPreview() { -    RepoStore.currentBlobView = 'repo-preview'; -  }, - -  // getters - -  isActiveFile(file) { -    return file && file.url === RepoStore.activeFile.url; -  }, - -  isPreviewView() { -    return RepoStore.currentBlobView === 'repo-preview'; -  }, -}; - -export default RepoStore; diff --git a/app/assets/javascripts/repo/stores/state.js b/app/assets/javascripts/repo/stores/state.js new file mode 100644 index 00000000000..0068834831e --- /dev/null +++ b/app/assets/javascripts/repo/stores/state.js @@ -0,0 +1,24 @@ +export default () => ({ +  canCommit: false, +  currentBranch: '', +  currentBlobView: 'repo-preview', +  currentRef: '', +  discardPopupOpen: false, +  editMode: false, +  endpoints: {}, +  isRoot: false, +  isInitialRoot: false, +  lastCommitPath: '', +  loading: false, +  onTopOfBranch: false, +  openFiles: [], +  path: '', +  project: { +    id: 0, +    name: '', +    url: '', +  }, +  parentTreeUrl: '', +  previousUrl: '', +  tree: [], +}); diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/repo/stores/utils.js new file mode 100644 index 00000000000..fae1f4439a9 --- /dev/null +++ b/app/assets/javascripts/repo/stores/utils.js @@ -0,0 +1,127 @@ +export const dataStructure = () => ({ +  id: '', +  key: '', +  type: '', +  name: '', +  url: '', +  path: '', +  level: 0, +  tempFile: false, +  icon: '', +  tree: [], +  loading: false, +  opened: false, +  active: false, +  changed: false, +  lastCommitPath: '', +  lastCommit: { +    url: '', +    message: '', +    updatedAt: '', +  }, +  tree_url: '', +  blamePath: '', +  commitsPath: '', +  permalink: '', +  rawPath: '', +  binary: false, +  html: '', +  raw: '', +  content: '', +  parentTreeUrl: '', +  renderError: false, +  base64: false, +}); + +export const decorateData = (entity) => { +  const { +    id, +    type, +    url, +    name, +    icon, +    tree_url, +    path, +    renderError, +    content = '', +    tempFile = false, +    active = false, +    opened = false, +    changed = false, +    parentTreeUrl = '', +    level = 0, +    base64 = false, +  } = entity; + +  return { +    ...dataStructure(), +    id, +    key: `${name}-${type}-${id}`, +    type, +    name, +    url, +    tree_url, +    path, +    level, +    tempFile, +    icon: `fa-${icon}`, +    opened, +    active, +    parentTreeUrl, +    changed, +    renderError, +    content, +    base64, +  }; +}; + +export const findEntry = (state, type, name) => state.tree.find( +  f => f.type === type && f.name === name, +); +export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path); + +export const setPageTitle = (title) => { +  document.title = title; +}; + +export const pushState = (url) => { +  history.pushState({ url }, '', url); +}; + +export const createTemp = ({ name, path, type, level, changed, content, base64 }) => { +  const treePath = path ? `${path}/${name}` : name; + +  return decorateData({ +    id: new Date().getTime().toString(), +    name, +    type, +    tempFile: true, +    path: treePath, +    icon: type === 'tree' ? 'folder' : 'file-text-o', +    changed, +    content, +    parentTreeUrl: '', +    level, +    base64, +    renderError: base64, +  }); +}; + +export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => { +  const found = findEntry(tree, type, entry.name); + +  if (found) { +    return Object.assign({}, found, { +      id: entry.id, +      url: entry.url, +      tempFile: false, +    }); +  } + +  return decorateData({ +    ...entry, +    type, +    parentTreeUrl, +    level, +  }); +}; diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 05caf177aec..07fee53d814 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -1,5 +1,5 @@  /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */ -/* global Flash */ +import Flash from './flash';  import Api from './api';  (function() { diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index f15452ec683..9dec5d7645a 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -162,13 +162,19 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.        items = [          {            header: "" + name -        }, { +        } +      ]; +      const issueItems = [ +        {            text: 'Issues assigned to me',            url: issuesPath + "/?assignee_username=" + userName          }, {            text: "Issues I've created",            url: issuesPath + "/?author_username=" + userName -        }, 'separator', { +        } +      ]; +      const mergeRequestItems = [ +        {            text: 'Merge requests assigned to me',            url: mrPath + "/?assignee_username=" + userName          }, { @@ -176,6 +182,11 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.            url: mrPath + "/?author_username=" + userName          }        ]; +      if (options.issuesDisabled) { +        items = items.concat(mergeRequestItems); +      } else { +        items = items.concat(...issueItems, 'separator', ...mergeRequestItems); +      }        if (!name) {          items.splice(0, 1);        } @@ -408,6 +419,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.        gl.projectOptions[projectPath] = {          name: $projectOptionsDataEl.data('name'),          issuesPath: $projectOptionsDataEl.data('issues-path'), +        issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),          mrPath: $projectOptionsDataEl.data('mr-path')        };      } diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index 8635ccece6e..d34a21b37e1 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -1,34 +1,26 @@ -function expandSectionParent($section, $content) { -  $section.addClass('expanded'); -  $content.off('animationend.expandSectionParent'); -} -  function expandSection($section) {    $section.find('.js-settings-toggle').text('Collapse'); - -  const $content = $section.find('.settings-content'); -  $content.addClass('expanded').off('scroll.expandSection').scrollTop(0); - -  if ($content.hasClass('no-animate')) { -    expandSectionParent($section, $content); -  } else { -    $content.on('animationend.expandSectionParent', () => expandSectionParent($section, $content)); +  $section.find('.settings-content').off('scroll.expandSection').scrollTop(0); +  $section.addClass('expanded'); +  if (!$section.hasClass('no-animate')) { +    $section.addClass('animating') +      .one('animationend.animateSection', () => $section.removeClass('animating'));    }  }  function closeSection($section) {    $section.find('.js-settings-toggle').text('Expand'); - -  const $content = $section.find('.settings-content'); -  $content.removeClass('expanded').on('scroll.expandSection', () => expandSection($section)); - +  $section.find('.settings-content').on('scroll.expandSection', () => expandSection($section));    $section.removeClass('expanded'); +  if (!$section.hasClass('no-animate')) { +    $section.addClass('animating') +      .one('animationend.animateSection', () => $section.removeClass('animating')); +  }  }  function toggleSection($section) { -  const $content = $section.find('.settings-content'); -  $content.removeClass('no-animate'); -  if ($content.hasClass('expanded')) { +  $section.removeClass('no-animate'); +  if ($section.hasClass('expanded')) {      closeSection($section);    } else {      expandSection($section); @@ -39,10 +31,19 @@ export default function initSettingsPanels() {    $('.settings').each((i, elm) => {      const $section = $(elm);      $section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section)); -    $section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section)); + +    if (!$section.hasClass('expanded')) { +      $section.find('.settings-content').on('scroll.expandSection', () => { +        $section.removeClass('no-animate'); +        expandSection($section); +      }); +    }    });    if (location.hash) { -    expandSection($(location.hash)); +    const $target = $(location.hash); +    if ($target.length && $target.hasClass('.settings')) { +      expandSection($target); +    }    }  } diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index f63b99ba1c5..ebe7a99ffae 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,128 +1,116 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */ -/* global Mousetrap */  import Cookies from 'js-cookie';  import Mousetrap from 'mousetrap'; -  import findAndFollowLink from './shortcuts_dashboard_navigation'; -(function() { -  this.Shortcuts = (function() { -    function Shortcuts(skipResetBindings) { -      this.onToggleHelp = this.onToggleHelp.bind(this); -      this.enabledHelp = []; -      if (!skipResetBindings) { -        Mousetrap.reset(); -      } -      Mousetrap.bind('?', this.onToggleHelp); -      Mousetrap.bind('s', Shortcuts.focusSearch); -      Mousetrap.bind('f', (e => this.focusFilter(e))); -      Mousetrap.bind('p b', this.onTogglePerfBar); - -      const findFileURL = document.body.dataset.findFile; - -      Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos')); -      Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity')); -      Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues')); -      Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests')); -      Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects')); -      Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups')); -      Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones')); -      Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets')); - -      Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview); -      if (typeof findFileURL !== "undefined" && findFileURL !== null) { -        Mousetrap.bind('t', function() { -          return gl.utils.visitUrl(findFileURL); -        }); -      } +const defaultStopCallback = Mousetrap.stopCallback; +Mousetrap.stopCallback = (e, element, combo) => { +  if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) { +    return false; +  } + +  return defaultStopCallback(e, element, combo); +}; + +export default class Shortcuts { +  constructor(skipResetBindings) { +    this.onToggleHelp = this.onToggleHelp.bind(this); +    this.enabledHelp = []; +    if (!skipResetBindings) { +      Mousetrap.reset();      } +    Mousetrap.bind('?', this.onToggleHelp); +    Mousetrap.bind('s', Shortcuts.focusSearch); +    Mousetrap.bind('f', this.focusFilter.bind(this)); +    Mousetrap.bind('p b', Shortcuts.onTogglePerfBar); -    Shortcuts.prototype.onToggleHelp = function(e) { -      e.preventDefault(); -      return Shortcuts.toggleHelp(this.enabledHelp); -    }; +    const findFileURL = document.body.dataset.findFile; + +    Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos')); +    Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity')); +    Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues')); +    Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests')); +    Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects')); +    Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups')); +    Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones')); +    Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets')); + +    Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], Shortcuts.toggleMarkdownPreview); -    Shortcuts.prototype.onTogglePerfBar = function(e) { +    if (typeof findFileURL !== 'undefined' && findFileURL !== null) { +      Mousetrap.bind('t', () => { +        gl.utils.visitUrl(findFileURL); +      }); +    } + +    $(document).on('click.more_help', '.js-more-help-button', function clickMoreHelp(e) { +      $(this).remove(); +      $('.hidden-shortcut').show();        e.preventDefault(); -      const performanceBarCookieName = 'perf_bar_enabled'; -      if (Cookies.get(performanceBarCookieName) === 'true') { -        Cookies.remove(performanceBarCookieName, { path: '/' }); -      } else { -        Cookies.set(performanceBarCookieName, 'true', { path: '/' }); -      } -      gl.utils.refreshCurrentPage(); -    }; - -    Shortcuts.prototype.toggleMarkdownPreview = function(e) { -      // Check if short-cut was triggered while in Write Mode -      const $target = $(e.target); -      const $form = $target.closest('form'); - -      if ($target.hasClass('js-note-text')) { -        $('.js-md-preview-button', $form).focus(); -      } -      return $(document).triggerHandler('markdown-preview:toggle', [e]); -    }; - -    Shortcuts.toggleHelp = function(location) { -      var $modal; -      $modal = $('#modal-shortcuts'); -      if ($modal.length) { -        $modal.modal('toggle'); -        return; -      } -      return $.ajax({ -        url: gon.shortcuts_path, -        dataType: 'script', -        success: function(e) { -          var i, l, len, results; -          if (location && location.length > 0) { -            results = []; -            for (i = 0, len = location.length; i < len; i += 1) { -              l = location[i]; -              results.push($(l).show()); -            } -            return results; -          } else { -            $('.hidden-shortcut').show(); -            return $('.js-more-help-button').remove(); +    }); +  } + +  onToggleHelp(e) { +    e.preventDefault(); +    Shortcuts.toggleHelp(this.enabledHelp); +  } + +  static onTogglePerfBar(e) { +    e.preventDefault(); +    const performanceBarCookieName = 'perf_bar_enabled'; +    if (Cookies.get(performanceBarCookieName) === 'true') { +      Cookies.remove(performanceBarCookieName, { path: '/' }); +    } else { +      Cookies.set(performanceBarCookieName, 'true', { path: '/' }); +    } +    gl.utils.refreshCurrentPage(); +  } + +  static toggleMarkdownPreview(e) { +    // Check if short-cut was triggered while in Write Mode +    const $target = $(e.target); +    const $form = $target.closest('form'); + +    if ($target.hasClass('js-note-text')) { +      $('.js-md-preview-button', $form).focus(); +    } +    $(document).triggerHandler('markdown-preview:toggle', [e]); +  } + +  static toggleHelp(location) { +    const $modal = $('#modal-shortcuts'); + +    if ($modal.length) { +      $modal.modal('toggle'); +    } + +    $.ajax({ +      url: gon.shortcuts_path, +      dataType: 'script', +      success() { +        if (location && location.length > 0) { +          const results = []; +          for (let i = 0, len = location.length; i < len; i += 1) { +            results.push($(location[i]).show());            } +          return results;          } -      }); -    }; - -    Shortcuts.prototype.focusFilter = function(e) { -      if (this.filterInput == null) { -        this.filterInput = $('input[type=search]', '.nav-controls'); -      } -      this.filterInput.focus(); -      return e.preventDefault(); -    }; - -    Shortcuts.focusSearch = function(e) { -      $('#search').focus(); -      return e.preventDefault(); -    }; - -    return Shortcuts; -  })(); - -  $(document).on('click.more_help', '.js-more-help-button', function(e) { -    $(this).remove(); -    $('.hidden-shortcut').show(); -    return e.preventDefault(); -  }); - -  Mousetrap.stopCallback = (function() { -    var defaultStopCallback; -    defaultStopCallback = Mousetrap.stopCallback; -    return function(e, element, combo) { -      // allowed shortcuts if textarea, input, contenteditable are focused -      if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) { -        return false; -      } else { -        return defaultStopCallback.apply(this, arguments); -      } -    }; -  })(); -}).call(window); + +        $('.hidden-shortcut').show(); +        return $('.js-more-help-button').remove(); +      }, +    }); +  } + +  focusFilter(e) { +    if (!this.filterInput) { +      this.filterInput = $('input[type=search]', '.nav-controls'); +    } +    this.filterInput.focus(); +    e.preventDefault(); +  } + +  static focusSearch(e) { +    $('#search').focus(); +    e.preventDefault(); +  } +} diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index ccbf7c59165..fbc57bb4304 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,7 +1,6 @@  /* global Mousetrap */ -/* global Shortcuts */ -import './shortcuts'; +import Shortcuts from './shortcuts';  const defaults = {    skipResetBindings: false, diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index b18b6139b35..81286c0010c 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -1,38 +1,30 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife */  /* global Mousetrap */ -/* global ShortcutsNavigation */ -import './shortcuts_navigation'; +import ShortcutsNavigation from './shortcuts_navigation'; -(function() { -  var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, -    hasProp = {}.hasOwnProperty; +export default class ShortcutsFindFile extends ShortcutsNavigation { +  constructor(projectFindFile) { +    super(); -  this.ShortcutsFindFile = (function(superClass) { -    extend(ShortcutsFindFile, superClass); +    const oldStopCallback = Mousetrap.stopCallback; +    this.projectFindFile = projectFindFile; -    function ShortcutsFindFile(projectFindFile) { -      var _oldStopCallback; -      this.projectFindFile = projectFindFile; -      ShortcutsFindFile.__super__.constructor.call(this); -      _oldStopCallback = Mousetrap.stopCallback; -      Mousetrap.stopCallback = (function(_this) { -        // override to fire shortcuts action when focus in textbox -        return function(event, element, combo) { -          if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) { -            // when press up/down key in textbox, cusor prevent to move to home/end -            event.preventDefault(); -            return false; -          } -          return _oldStopCallback(event, element, combo); -        }; -      })(this); -      Mousetrap.bind('up', this.projectFindFile.selectRowUp); -      Mousetrap.bind('down', this.projectFindFile.selectRowDown); -      Mousetrap.bind('esc', this.projectFindFile.goToTree); -      Mousetrap.bind('enter', this.projectFindFile.goToBlob); -    } +    Mousetrap.stopCallback = (e, element, combo) => { +      if ( +        element === this.projectFindFile.inputElement[0] && +        (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter') +      ) { +        // when press up/down key in textbox, cusor prevent to move to home/end +        event.preventDefault(); +        return false; +      } -    return ShortcutsFindFile; -  })(ShortcutsNavigation); -}).call(window); +      return oldStopCallback(e, element, combo); +    }; + +    Mousetrap.bind('up', this.projectFindFile.selectRowUp); +    Mousetrap.bind('down', this.projectFindFile.selectRowDown); +    Mousetrap.bind('esc', this.projectFindFile.goToTree); +    Mousetrap.bind('enter', this.projectFindFile.goToBlob); +  } +} diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 78b257bf192..4f4f606d293 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,100 +1,75 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */  /* global Mousetrap */ -/* global ShortcutsNavigation */  /* global sidebar */  import _ from 'underscore';  import 'mousetrap'; -import './shortcuts_navigation'; - -(function() { -  var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, -    hasProp = {}.hasOwnProperty; - -  this.ShortcutsIssuable = (function(superClass) { -    extend(ShortcutsIssuable, superClass); - -    function ShortcutsIssuable(isMergeRequest) { -      ShortcutsIssuable.__super__.constructor.call(this); -      Mousetrap.bind('a', this.openSidebarDropdown.bind(this, 'assignee')); -      Mousetrap.bind('m', this.openSidebarDropdown.bind(this, 'milestone')); -      Mousetrap.bind('r', (function(_this) { -        return function() { -          _this.replyWithSelectedText(isMergeRequest); -          return false; -        }; -      })(this)); -      Mousetrap.bind('e', (function(_this) { -        return function() { -          _this.editIssue(); -          return false; -        }; -      })(this)); -      Mousetrap.bind('l', this.openSidebarDropdown.bind(this, 'labels')); -      if (isMergeRequest) { -        this.enabledHelp.push('.hidden-shortcut.merge_requests'); -      } else { -        this.enabledHelp.push('.hidden-shortcut.issues'); -      } +import ShortcutsNavigation from './shortcuts_navigation'; +import { CopyAsGFM } from './behaviors/copy_as_gfm'; + +export default class ShortcutsIssuable extends ShortcutsNavigation { +  constructor(isMergeRequest) { +    super(); + +    this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form'); +    this.editBtn = document.querySelector('.issuable-edit'); + +    Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee')); +    Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone')); +    Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels')); +    Mousetrap.bind('r', this.replyWithSelectedText.bind(this)); +    Mousetrap.bind('e', this.editIssue.bind(this)); + +    if (isMergeRequest) { +      this.enabledHelp.push('.hidden-shortcut.merge_requests'); +    } else { +      this.enabledHelp.push('.hidden-shortcut.issues');      } +  } -    ShortcutsIssuable.prototype.replyWithSelectedText = function(isMergeRequest) { -      var quote, documentFragment, el, selected, separator; -      let replyField; - -      if (isMergeRequest) { -        replyField = $('.js-main-target-form #note_note'); -      } else { -        replyField = $('.js-main-target-form .js-vue-comment-form'); -      } - -      documentFragment = window.gl.utils.getSelectedFragment(); -      if (!documentFragment) { -        replyField.focus(); -        return; -      } - -      el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); -      selected = window.gl.CopyAsGFM.nodeToGFM(el); - -      if (selected.trim() === "") { -        return; -      } -      quote = _.map(selected.split("\n"), function(val) { -        return ("> " + val).trim() + "\n"; -      }); - -      // If replyField already has some content, add a newline before our quote -      separator = replyField.val().trim() !== "" && "\n\n" || ''; -      replyField.val(function(a, current) { -        return current + separator + quote.join('') + "\n"; -      }); - -      // Trigger autosave -      replyField.trigger('input').trigger('change'); - -      // Trigger autosize -      var event = document.createEvent('Event'); -      event.initEvent('autosize:update', true, false); -      replyField.get(0).dispatchEvent(event); - -      // Focus the input field -      return replyField.focus(); -    }; - -    ShortcutsIssuable.prototype.editIssue = function() { -      var $editBtn; -      $editBtn = $('.issuable-edit'); -      // Need to click the element as on issues, editing is inline -      // on merge request, editing is on a different page -      $editBtn.get(0).click(); -    }; - -    ShortcutsIssuable.prototype.openSidebarDropdown = function(name) { -      sidebar.openDropdown(name); +  replyWithSelectedText() { +    const documentFragment = window.gl.utils.getSelectedFragment(); + +    if (!documentFragment) { +      this.$replyField.focus();        return false; -    }; +    } + +    const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); +    const selected = CopyAsGFM.nodeToGFM(el); + +    if (selected.trim() === '') { +      return false; +    } + +    const quote = _.map(selected.split('\n'), val => `${(`> ${val}`).trim()}\n`); + +    // If replyField already has some content, add a newline before our quote +    const separator = (this.$replyField.val().trim() !== '' && '\n\n') || ''; +    this.$replyField.val((a, current) => `${current}${separator}${quote.join('')}\n`) +      .trigger('input') +      .trigger('change'); + +    // Trigger autosize +    const event = document.createEvent('Event'); +    event.initEvent('autosize:update', true, false); +    this.$replyField.get(0).dispatchEvent(event); + +    // Focus the input field +    this.$replyField.focus(); + +    return false; +  } + +  editIssue() { +    // Need to click the element as on issues, editing is inline +    // on merge request, editing is on a different page +    this.editBtn.click(); + +    return false; +  } -    return ShortcutsIssuable; -  })(ShortcutsNavigation); -}).call(window); +  static openSidebarDropdown(name) { +    sidebar.openDropdown(name); +    return false; +  } +} diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 55bae0c08a1..b4562701a3e 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,36 +1,27 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */  /* global Mousetrap */ -/* global Shortcuts */  import findAndFollowLink from './shortcuts_dashboard_navigation'; -import './shortcuts'; +import Shortcuts from './shortcuts'; -(function() { -  var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, -    hasProp = {}.hasOwnProperty; +export default class ShortcutsNavigation extends Shortcuts { +  constructor() { +    super(); -  this.ShortcutsNavigation = (function(superClass) { -    extend(ShortcutsNavigation, superClass); +    Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project')); +    Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity')); +    Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree')); +    Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits')); +    Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds')); +    Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network')); +    Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts')); +    Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues')); +    Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards')); +    Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests')); +    Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos')); +    Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki')); +    Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); +    Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue')); -    function ShortcutsNavigation() { -      ShortcutsNavigation.__super__.constructor.call(this); -      Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project')); -      Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity')); -      Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree')); -      Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits')); -      Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds')); -      Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network')); -      Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts')); -      Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues')); -      Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards')); -      Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests')); -      Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos')); -      Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki')); -      Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); -      Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue')); -      this.enabledHelp.push('.hidden-shortcut.project'); -    } - -    return ShortcutsNavigation; -  })(Shortcuts); -}).call(window); +    this.enabledHelp.push('.hidden-shortcut.project'); +  } +} diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index cc44082efa9..21823085ac4 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -1,28 +1,17 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, max-len */  /* global Mousetrap */ -/* global ShortcutsNavigation */ +import ShortcutsNavigation from './shortcuts_navigation'; -import './shortcuts_navigation'; +export default class ShortcutsNetwork extends ShortcutsNavigation { +  constructor(graph) { +    super(); -(function() { -  var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, -    hasProp = {}.hasOwnProperty; +    Mousetrap.bind(['left', 'h'], graph.scrollLeft); +    Mousetrap.bind(['right', 'l'], graph.scrollRight); +    Mousetrap.bind(['up', 'k'], graph.scrollUp); +    Mousetrap.bind(['down', 'j'], graph.scrollDown); +    Mousetrap.bind(['shift+up', 'shift+k'], graph.scrollTop); +    Mousetrap.bind(['shift+down', 'shift+j'], graph.scrollBottom); -  this.ShortcutsNetwork = (function(superClass) { -    extend(ShortcutsNetwork, superClass); - -    function ShortcutsNetwork(graph) { -      this.graph = graph; -      ShortcutsNetwork.__super__.constructor.call(this); -      Mousetrap.bind(['left', 'h'], this.graph.scrollLeft); -      Mousetrap.bind(['right', 'l'], this.graph.scrollRight); -      Mousetrap.bind(['up', 'k'], this.graph.scrollUp); -      Mousetrap.bind(['down', 'j'], this.graph.scrollDown); -      Mousetrap.bind(['shift+up', 'shift+k'], this.graph.scrollTop); -      Mousetrap.bind(['shift+down', 'shift+j'], this.graph.scrollBottom); -      this.enabledHelp.push('.hidden-shortcut.network'); -    } - -    return ShortcutsNetwork; -  })(ShortcutsNavigation); -}).call(window); +    this.enabledHelp.push('.hidden-shortcut.network'); +  } +} diff --git a/app/assets/javascripts/shortcuts_wiki.js b/app/assets/javascripts/shortcuts_wiki.js index 8a075062a48..59b967dbe09 100644 --- a/app/assets/javascripts/shortcuts_wiki.js +++ b/app/assets/javascripts/shortcuts_wiki.js @@ -1,7 +1,7 @@  /* eslint-disable class-methods-use-this */  /* global Mousetrap */ -/* global ShortcutsNavigation */ +import ShortcutsNavigation from './shortcuts_navigation';  import findAndFollowLink from './shortcuts_dashboard_navigation';  export default class ShortcutsWiki extends ShortcutsNavigation { diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js index f83c3b037ed..74c17bc14a2 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js @@ -1,5 +1,4 @@ -/* global Flash */ - +import Flash from '../../../flash';  import AssigneeTitle from './assignee_title';  import Assignees from './assignees'; diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index f2b1099a678..22a9a34dda3 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -1,5 +1,5 @@  <script> -/* global Flash */ +import Flash from '../../../flash';  import editForm from './edit_form.vue';  export default { diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue new file mode 100644 index 00000000000..b8510a6ce3a --- /dev/null +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -0,0 +1,125 @@ +<script> +import { __, n__, sprintf } from '../../../locale'; +import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; +import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue'; + +export default { +  props: { +    loading: { +      type: Boolean, +      required: false, +      default: false, +    }, +    participants: { +      type: Array, +      required: false, +      default: () => [], +    }, +    numberOfLessParticipants: { +      type: Number, +      required: false, +      default: 7, +    }, +  }, +  data() { +    return { +      isShowingMoreParticipants: false, +    }; +  }, +  components: { +    loadingIcon, +    userAvatarImage, +  }, +  computed: { +    lessParticipants() { +      return this.participants.slice(0, this.numberOfLessParticipants); +    }, +    visibleParticipants() { +      return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; +    }, +    hasMoreParticipants() { +      return this.participants.length > this.numberOfLessParticipants; +    }, +    toggleLabel() { +      let label = ''; +      if (this.isShowingMoreParticipants) { +        label = __('- show less'); +      } else { +        label = sprintf(__('+ %{moreCount} more'), { +          moreCount: this.participants.length - this.numberOfLessParticipants, +        }); +      } + +      return label; +    }, +    participantLabel() { +      return sprintf( +        n__('%{count} participant', '%{count} participants', this.participants.length), +        { count: this.loading ? '' : this.participantCount }, +      ); +    }, +    participantCount() { +      return this.participants.length; +    }, +  }, +  methods: { +    toggleMoreParticipants() { +      this.isShowingMoreParticipants = !this.isShowingMoreParticipants; +    }, +  }, +}; +</script> + +<template> +  <div> +    <div class="sidebar-collapsed-icon"> +      <i +        class="fa fa-users" +        aria-hidden="true"> +      </i> +      <loading-icon +        v-if="loading" +        class="js-participants-collapsed-loading-icon" /> +      <span +        v-else +        class="js-participants-collapsed-count"> +        {{ participantCount }} +      </span> +    </div> +    <div class="title hide-collapsed"> +      <loading-icon +        v-if="loading" +        :inline="true" +        class="js-participants-expanded-loading-icon" /> +      {{ participantLabel }} +    </div> +    <div class="participants-list hide-collapsed"> +      <div +        v-for="participant in visibleParticipants" +        :key="participant.id" +        class="participants-author js-participants-author"> +        <a +          class="author_link" +          :href="participant.web_url"> +          <user-avatar-image +            :lazy="true" +            :img-src="participant.avatar_url" +            css-classes="avatar-inline" +            :size="24" +            :tooltip-text="participant.name" +            tooltip-placement="bottom" /> +        </a> +      </div> +    </div> +    <div +      v-if="hasMoreParticipants" +      class="participants-more hide-collapsed"> +      <button +        type="button" +        class="btn-transparent btn-blank js-toggle-participants-button" +        @click="toggleMoreParticipants"> +        {{ toggleLabel }} +      </button> +    </div> +  </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue new file mode 100644 index 00000000000..c1296b28db7 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue @@ -0,0 +1,26 @@ +<script> +import Store from '../../stores/sidebar_store'; +import Mediator from '../../sidebar_mediator'; +import participants from './participants.vue'; + +export default { +  data() { +    return { +      mediator: new Mediator(), +      store: new Store(), +    }; +  }, +  components: { +    participants, +  }, +}; +</script> + +<template> +  <div class="block participants"> +    <participants +      :loading="store.isFetching.participants" +      :participants="store.participants" +      :number-of-less-participants="7" /> +  </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue new file mode 100644 index 00000000000..4ad3d469f25 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue @@ -0,0 +1,45 @@ +<script> +import Store from '../../stores/sidebar_store'; +import Mediator from '../../sidebar_mediator'; +import eventHub from '../../event_hub'; +import Flash from '../../../flash'; +import subscriptions from './subscriptions.vue'; + +export default { +  data() { +    return { +      mediator: new Mediator(), +      store: new Store(), +    }; +  }, + +  components: { +    subscriptions, +  }, + +  methods: { +    onToggleSubscription() { +      this.mediator.toggleSubscription() +        .catch(() => { +          Flash('Error occurred when toggling the notification subscription'); +        }); +    }, +  }, + +  created() { +    eventHub.$on('toggleSubscription', this.onToggleSubscription); +  }, + +  beforeDestroy() { +    eventHub.$off('toggleSubscription', this.onToggleSubscription); +  }, +}; +</script> + +<template> +  <div class="block subscriptions"> +    <subscriptions +      :loading="store.isFetching.subscriptions" +      :subscribed="store.subscribed" /> +  </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue new file mode 100644 index 00000000000..a3a8213d63a --- /dev/null +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -0,0 +1,60 @@ +<script> +import { __ } from '../../../locale'; +import eventHub from '../../event_hub'; +import loadingButton from '../../../vue_shared/components/loading_button.vue'; + +export default { +  props: { +    loading: { +      type: Boolean, +      required: false, +      default: false, +    }, +    subscribed: { +      type: Boolean, +      required: false, +    }, +  }, +  components: { +    loadingButton, +  }, +  computed: { +    buttonLabel() { +      let label; +      if (this.subscribed === false) { +        label = __('Subscribe'); +      } else if (this.subscribed === true) { +        label = __('Unsubscribe'); +      } + +      return label; +    }, +  }, +  methods: { +    toggleSubscription() { +      eventHub.$emit('toggleSubscription'); +    }, +  }, +}; +</script> + +<template> +  <div> +    <div class="sidebar-collapsed-icon"> +      <i +        class="fa fa-rss" +        aria-hidden="true"> +      </i> +    </div> +    <span class="issuable-header-text hide-collapsed pull-left"> +      {{ __('Notifications') }} +    </span> +    <loading-button +      ref="loadingButton" +      class="btn btn-default pull-right hide-collapsed js-issuable-subscribe-button" +      :loading="loading" +      :label="buttonLabel" +      @click="toggleSubscription" +    /> +  </div> +</template> diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index 3c9de02407e..977dd83a7ea 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -1,5 +1,3 @@ -/* global Flash */ -  function isValidProjectId(id) {    return id > 0;  } @@ -38,7 +36,7 @@ class SidebarMoveIssue {        data: (searchTerm, callback) => {          this.mediator.fetchAutocompleteProjects(searchTerm)            .then(callback) -          .catch(() => new Flash('An error occurred while fetching projects autocomplete.')); +          .catch(() => new window.Flash('An error occurred while fetching projects autocomplete.'));        },        renderRow: project => `          <li> @@ -73,7 +71,7 @@ class SidebarMoveIssue {        this.mediator.moveIssue()          .catch(() => { -          Flash('An error occurred while moving the issue.'); +          window.Flash('An error occurred while moving the issue.');            this.$confirmButton              .enable()              .removeClass('is-loading'); diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js index 604648407a4..37c97225bfd 100644 --- a/app/assets/javascripts/sidebar/services/sidebar_service.js +++ b/app/assets/javascripts/sidebar/services/sidebar_service.js @@ -7,6 +7,7 @@ export default class SidebarService {    constructor(endpointMap) {      if (!SidebarService.singleton) {        this.endpoint = endpointMap.endpoint; +      this.toggleSubscriptionEndpoint = endpointMap.toggleSubscriptionEndpoint;        this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;        this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint; @@ -36,6 +37,10 @@ export default class SidebarService {      });    } +  toggleSubscription() { +    return Vue.http.post(this.toggleSubscriptionEndpoint); +  } +    moveIssue(moveToProjectId) {      return Vue.http.post(this.moveIssueEndpoint, {        move_to_project_id: moveToProjectId, diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js index 09b9d75c02d..2650bb725d4 100644 --- a/app/assets/javascripts/sidebar/sidebar_bundle.js +++ b/app/assets/javascripts/sidebar/sidebar_bundle.js @@ -4,6 +4,8 @@ import SidebarAssignees from './components/assignees/sidebar_assignees';  import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';  import SidebarMoveIssue from './lib/sidebar_move_issue';  import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue'; +import sidebarParticipants from './components/participants/sidebar_participants.vue'; +import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';  import Translate from '../vue_shared/translate';  import Mediator from './sidebar_mediator'; @@ -49,6 +51,36 @@ function mountLockComponent(mediator) {    }).$mount(el);  } +function mountParticipantsComponent() { +  const el = document.querySelector('.js-sidebar-participants-entry-point'); + +  if (!el) return; + +  // eslint-disable-next-line no-new +  new Vue({ +    el, +    components: { +      sidebarParticipants, +    }, +    render: createElement => createElement('sidebar-participants', {}), +  }); +} + +function mountSubscriptionsComponent() { +  const el = document.querySelector('.js-sidebar-subscriptions-entry-point'); + +  if (!el) return; + +  // eslint-disable-next-line no-new +  new Vue({ +    el, +    components: { +      sidebarSubscriptions, +    }, +    render: createElement => createElement('sidebar-subscriptions', {}), +  }); +} +  function domContentLoaded() {    const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);    const mediator = new Mediator(sidebarOptions); @@ -63,6 +95,8 @@ function domContentLoaded() {    mountConfidentialComponent(mediator);    mountLockComponent(mediator); +  mountParticipantsComponent(); +  mountSubscriptionsComponent();    new SidebarMoveIssue(      mediator, diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index 2fe6e5b31f0..2bda5a47791 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -1,5 +1,4 @@ -/* global Flash */ - +import Flash from '../flash';  import Service from './services/sidebar_service';  import Store from './stores/sidebar_store'; @@ -9,6 +8,7 @@ export default class SidebarMediator {        this.store = new Store(options);        this.service = new Service({          endpoint: options.endpoint, +        toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,          moveIssueEndpoint: options.moveIssueEndpoint,          projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,        }); @@ -40,10 +40,25 @@ export default class SidebarMediator {        .then((data) => {          this.store.setAssigneeData(data);          this.store.setTimeTrackingData(data); +        this.store.setParticipantsData(data); +        this.store.setSubscriptionsData(data);        })        .catch(() => new Flash('Error occurred when fetching sidebar data'));    } +  toggleSubscription() { +    this.store.setFetchingState('subscriptions', true); +    return this.service.toggleSubscription() +      .then(() => { +        this.store.setSubscribedState(!this.store.subscribed); +        this.store.setFetchingState('subscriptions', false); +      }) +      .catch((err) => { +        this.store.setFetchingState('subscriptions', false); +        throw err; +      }); +  } +    fetchAutocompleteProjects(searchTerm) {      return this.service.getProjectsAutocomplete(searchTerm)        .then(response => response.json()) diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js index d5d04103f3f..3150221b685 100644 --- a/app/assets/javascripts/sidebar/stores/sidebar_store.js +++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js @@ -12,10 +12,14 @@ export default class SidebarStore {        this.assignees = [];        this.isFetching = {          assignees: true, +        participants: true, +        subscriptions: true,        };        this.autocompleteProjects = [];        this.moveToProjectId = 0;        this.isLockDialogOpen = false; +      this.participants = []; +      this.subscribed = null;        SidebarStore.singleton = this;      } @@ -37,6 +41,20 @@ export default class SidebarStore {      this.humanTotalTimeSpent = data.human_total_time_spent;    } +  setParticipantsData(data) { +    this.isFetching.participants = false; +    this.participants = data.participants || []; +  } + +  setSubscriptionsData(data) { +    this.isFetching.subscriptions = false; +    this.subscribed = data.subscribed || false; +  } + +  setFetchingState(key, value) { +    this.isFetching[key] = value; +  } +    addAssignee(assignee) {      if (!this.findAssignee(assignee)) {        this.assignees.push(assignee); @@ -61,6 +79,10 @@ export default class SidebarStore {      this.autocompleteProjects = projects;    } +  setSubscribedState(subscribed) { +    this.subscribed = subscribed; +  } +    setMoveToProjectId(moveToProjectId) {      this.moveToProjectId = moveToProjectId;    } diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js index 2bf7a3a5d61..8e931995fc6 100644 --- a/app/assets/javascripts/smart_interval.js +++ b/app/assets/javascripts/smart_interval.js @@ -3,9 +3,10 @@   * and controllable by a public API.   */ -class SmartInterval { +export default class SmartInterval {    /** -   * @param { function } opts.callback Function to be called on each iteration (required) +   * @param { function } opts.callback Function that returns a promise, called on each iteration +   *                     unless still in progress (required)     * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially     * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this     * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this @@ -42,13 +43,16 @@ class SmartInterval {      const cfg = this.cfg;      const state = this.state; -    if (cfg.immediateExecution) { +    if (cfg.immediateExecution && !this.isLoading) {        cfg.immediateExecution = false; -      cfg.callback(); +      this.triggerCallback();      }      state.intervalId = window.setInterval(() => { -      cfg.callback(); +      if (this.isLoading) { +        return; +      } +      this.triggerCallback();        if (this.getCurrentInterval() === cfg.maxInterval) {          return; @@ -76,7 +80,7 @@ class SmartInterval {    // start a timer, using the existing interval    resume() { -    this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped +    this.stopTimer(); // stop existing timer, in case timer was not previously stopped      this.start();    } @@ -104,6 +108,18 @@ class SmartInterval {      this.initPageUnloadHandling();    } +  triggerCallback() { +    this.isLoading = true; +    this.cfg.callback() +      .then(() => { +        this.isLoading = false; +      }) +      .catch((err) => { +        this.isLoading = false; +        throw err; +      }); +  } +    initVisibilityChangeHandling() {      // cancel interval when tab no longer shown (prevents cached pages from polling)      document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); @@ -154,4 +170,3 @@ class SmartInterval {    }  } -window.gl.SmartInterval = SmartInterval; diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 3a06b477d7c..1a8dc085772 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,28 +1,29 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */ -/* global Flash */ - +import Flash from './flash';  import { __, s__ } from './locale';  export default class Star {    constructor() { -    $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { -      var $starIcon, $starSpan, $this, toggleStar; -      $this = $(this); -      $starSpan = $this.find('span'); -      $starIcon = $this.find('i'); -      toggleStar = function(isStarred) { -        $this.parent().find('.star-count').text(data.star_count); -        if (isStarred) { -          $starSpan.removeClass('starred').text(s__('StarProject|Star')); -          $starIcon.removeClass('fa-star').addClass('fa-star-o'); -        } else { -          $starSpan.addClass('starred').text(__('Unstar')); -          $starIcon.removeClass('fa-star-o').addClass('fa-star'); +    $('.project-home-panel .toggle-star') +      .on('ajax:success', function handleSuccess(e, data) { +        const $this = $(this); +        const $starSpan = $this.find('span'); +        const $starIcon = $this.find('i'); + +        function toggleStar(isStarred) { +          $this.parent().find('.star-count').text(data.star_count); +          if (isStarred) { +            $starSpan.removeClass('starred').text(s__('StarProject|Star')); +            $starIcon.removeClass('fa-star').addClass('fa-star-o'); +          } else { +            $starSpan.addClass('starred').text(__('Unstar')); +            $starIcon.removeClass('fa-star-o').addClass('fa-star'); +          }          } -      }; -      toggleStar($starSpan.hasClass('starred')); -    }).on('ajax:error', function(e, xhr, status, error) { -      new Flash('Star toggle failed. Try again later.', 'alert'); -    }); + +        toggleStar($starSpan.hasClass('starred')); +      }) +      .on('ajax:error', () => { +        Flash('Star toggle failed. Try again later.', 'alert'); +      });    }  } diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index c39f569da5e..dcbec40c79e 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -1,6 +1,5 @@ -/* global Flash */ -  import 'deckar01-task_list'; +import Flash from './flash';  export default class TaskList {    constructor(options = {}) { diff --git a/app/assets/javascripts/test_utils/index.js b/app/assets/javascripts/test_utils/index.js index 8875590f0f2..a55a338eea8 100644 --- a/app/assets/javascripts/test_utils/index.js +++ b/app/assets/javascripts/test_utils/index.js @@ -1,6 +1,8 @@  import 'core-js/es6/map';  import 'core-js/es6/set';  import simulateDrag from './simulate_drag'; +import simulateInput from './simulate_input';  // Export to global space for rspec to use  window.simulateDrag = simulateDrag; +window.simulateInput = simulateInput; diff --git a/app/assets/javascripts/test_utils/simulate_input.js b/app/assets/javascripts/test_utils/simulate_input.js new file mode 100644 index 00000000000..90c1b7cb57e --- /dev/null +++ b/app/assets/javascripts/test_utils/simulate_input.js @@ -0,0 +1,23 @@ +function triggerEvents(input) { +  input.dispatchEvent(new Event('keydown')); +  input.dispatchEvent(new Event('keypress')); +  input.dispatchEvent(new Event('input')); +  input.dispatchEvent(new Event('keyup')); +} + +export default function simulateInput(target, text) { +  const input = document.querySelector(target); +  if (!input || !input.matches('textarea, input')) { +    return false; +  } + +  if (text.length > 0) { +    Array.prototype.forEach.call(text, (char) => { +      input.value += char; +      triggerEvents(input); +    }); +  } else { +    triggerEvents(input); +  } +  return true; +} diff --git a/app/assets/javascripts/two_factor_auth.js b/app/assets/javascripts/two_factor_auth.js index d26f61562a5..e3414d9afff 100644 --- a/app/assets/javascripts/two_factor_auth.js +++ b/app/assets/javascripts/two_factor_auth.js @@ -1,4 +1,5 @@ -/* global U2FRegister */ +import U2FRegister from './u2f/register'; +  document.addEventListener('DOMContentLoaded', () => {    const twoFactorNode = document.querySelector('.js-two-factor-auth');    const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true'; diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index 8821b22477f..a3cc04e35fe 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -1,118 +1,108 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */ +/* eslint-disable func-names, wrap-iife */  /* global u2f */ -/* global U2FError */ -/* global U2FUtil */ -  import _ from 'underscore'; +import isU2FSupported from './util'; +import U2FError from './error';  // Authenticate U2F (universal 2nd factor) devices for users to authenticate with.  //  // State Flow #1: setup -> in_progress -> authenticated -> POST to server  // State Flow #2: setup -> in_progress -> error -> setup -(function() { -  const global = window.gl || (window.gl = {}); - -  global.U2FAuthenticate = (function() { -    function U2FAuthenticate(container, form, u2fParams, fallbackButton, fallbackUI) { -      this.container = container; -      this.renderNotSupported = this.renderNotSupported.bind(this); -      this.renderAuthenticated = this.renderAuthenticated.bind(this); -      this.renderError = this.renderError.bind(this); -      this.renderInProgress = this.renderInProgress.bind(this); -      this.renderTemplate = this.renderTemplate.bind(this); -      this.authenticate = this.authenticate.bind(this); -      this.start = this.start.bind(this); -      this.appId = u2fParams.app_id; -      this.challenge = u2fParams.challenge; -      this.form = form; -      this.fallbackButton = fallbackButton; -      this.fallbackUI = fallbackUI; -      if (this.fallbackButton) this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this)); -      this.signRequests = u2fParams.sign_requests.map(function(request) { -        // The U2F Javascript API v1.1 requires a single challenge, with -        // _no challenges per-request_. The U2F Javascript API v1.0 requires a -        // challenge per-request, which is done by copying the single challenge -        // into every request. -        // -        // In either case, we don't need the per-request challenges that the server -        // has generated, so we can remove them. -        // -        // Note: The server library fixes this behaviour in (unreleased) version 1.0.0. -        // This can be removed once we upgrade. -        // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4 -        return _(request).omit('challenge'); -      }); +export default class U2FAuthenticate { +  constructor(container, form, u2fParams, fallbackButton, fallbackUI) { +    this.container = container; +    this.renderNotSupported = this.renderNotSupported.bind(this); +    this.renderAuthenticated = this.renderAuthenticated.bind(this); +    this.renderError = this.renderError.bind(this); +    this.renderInProgress = this.renderInProgress.bind(this); +    this.renderTemplate = this.renderTemplate.bind(this); +    this.authenticate = this.authenticate.bind(this); +    this.start = this.start.bind(this); +    this.appId = u2fParams.app_id; +    this.challenge = u2fParams.challenge; +    this.form = form; +    this.fallbackButton = fallbackButton; +    this.fallbackUI = fallbackUI; +    if (this.fallbackButton) { +      this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));      } -    U2FAuthenticate.prototype.start = function() { -      if (U2FUtil.isU2FSupported()) { -        return this.renderInProgress(); -      } else { -        return this.renderNotSupported(); -      } -    }; +    // The U2F Javascript API v1.1 requires a single challenge, with +    // _no challenges per-request_. The U2F Javascript API v1.0 requires a +    // challenge per-request, which is done by copying the single challenge +    // into every request. +    // +    // In either case, we don't need the per-request challenges that the server +    // has generated, so we can remove them. +    // +    // Note: The server library fixes this behaviour in (unreleased) version 1.0.0. +    // This can be removed once we upgrade. +    // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4 +    this.signRequests = u2fParams.sign_requests.map(request => _(request).omit('challenge')); -    U2FAuthenticate.prototype.authenticate = function() { -      return u2f.sign(this.appId, this.challenge, this.signRequests, (function(_this) { -        return function(response) { -          var error; -          if (response.errorCode) { -            error = new U2FError(response.errorCode, 'authenticate'); -            return _this.renderError(error); -          } else { -            return _this.renderAuthenticated(JSON.stringify(response)); -          } -        }; -      })(this), 10); +    this.templates = { +      notSupported: '#js-authenticate-u2f-not-supported', +      setup: '#js-authenticate-u2f-setup', +      inProgress: '#js-authenticate-u2f-in-progress', +      error: '#js-authenticate-u2f-error', +      authenticated: '#js-authenticate-u2f-authenticated',      }; +  } -    // Rendering # -    U2FAuthenticate.prototype.templates = { -      "notSupported": "#js-authenticate-u2f-not-supported", -      "setup": '#js-authenticate-u2f-setup', -      "inProgress": '#js-authenticate-u2f-in-progress', -      "error": '#js-authenticate-u2f-error', -      "authenticated": '#js-authenticate-u2f-authenticated' -    }; +  start() { +    if (isU2FSupported()) { +      return this.renderInProgress(); +    } +    return this.renderNotSupported(); +  } -    U2FAuthenticate.prototype.renderTemplate = function(name, params) { -      var template, templateString; -      templateString = $(this.templates[name]).html(); -      template = _.template(templateString); -      return this.container.html(template(params)); -    }; +  authenticate() { +    return u2f.sign(this.appId, this.challenge, this.signRequests, (function (_this) { +      return function (response) { +        if (response.errorCode) { +          const error = new U2FError(response.errorCode, 'authenticate'); +          return _this.renderError(error); +        } +        return _this.renderAuthenticated(JSON.stringify(response)); +      }; +    })(this), 10); +  } -    U2FAuthenticate.prototype.renderInProgress = function() { -      this.renderTemplate('inProgress'); -      return this.authenticate(); -    }; +  renderTemplate(name, params) { +    const templateString = $(this.templates[name]).html(); +    const template = _.template(templateString); +    return this.container.html(template(params)); +  } -    U2FAuthenticate.prototype.renderError = function(error) { -      this.renderTemplate('error', { -        error_message: error.message(), -        error_code: error.errorCode -      }); -      return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress); -    }; +  renderInProgress() { +    this.renderTemplate('inProgress'); +    return this.authenticate(); +  } -    U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) { -      this.renderTemplate('authenticated'); -      const container = this.container[0]; -      container.querySelector('#js-device-response').value = deviceResponse; -      container.querySelector(this.form).submit(); -      this.fallbackButton.classList.add('hidden'); -    }; +  renderError(error) { +    this.renderTemplate('error', { +      error_message: error.message(), +      error_code: error.errorCode, +    }); +    return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress); +  } -    U2FAuthenticate.prototype.renderNotSupported = function() { -      return this.renderTemplate('notSupported'); -    }; +  renderAuthenticated(deviceResponse) { +    this.renderTemplate('authenticated'); +    const container = this.container[0]; +    container.querySelector('#js-device-response').value = deviceResponse; +    container.querySelector(this.form).submit(); +    this.fallbackButton.classList.add('hidden'); +  } -    U2FAuthenticate.prototype.switchToFallbackUI = function() { -      this.fallbackButton.classList.add('hidden'); -      this.container[0].classList.add('hidden'); -      this.fallbackUI.classList.remove('hidden'); -    }; +  renderNotSupported() { +    return this.renderTemplate('notSupported'); +  } + +  switchToFallbackUI() { +    this.fallbackButton.classList.add('hidden'); +    this.container[0].classList.add('hidden'); +    this.fallbackUI.classList.remove('hidden'); +  } -    return U2FAuthenticate; -  })(); -})(); +} diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index 3119b3480c3..1a98564ff55 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -1,25 +1,22 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, max-len */ -/* global u2f */ +export default class U2FError { +  constructor(errorCode, u2fFlowType) { +    this.errorCode = errorCode; +    this.message = this.message.bind(this); +    this.httpsDisabled = window.location.protocol !== 'https:'; +    this.u2fFlowType = u2fFlowType; +  } -(function() { -  this.U2FError = (function() { -    function U2FError(errorCode, u2fFlowType) { -      this.errorCode = errorCode; -      this.message = this.message.bind(this); -      this.httpsDisabled = window.location.protocol !== 'https:'; -      this.u2fFlowType = u2fFlowType; -    } - -    U2FError.prototype.message = function() { -      if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) { -        return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.'; -      } else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) { -        if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.'; -        if (this.u2fFlowType === 'register') return 'This device has already been registered with us.'; +  message() { +    if (this.errorCode === window.u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) { +      return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.'; +    } else if (this.errorCode === window.u2f.ErrorCodes.DEVICE_INELIGIBLE) { +      if (this.u2fFlowType === 'authenticate') { +        return 'This device has not been registered with us.';        } -      return "There was a problem communicating with your device."; -    }; - -    return U2FError; -  })(); -}).call(window); +      if (this.u2fFlowType === 'register') { +        return 'This device has already been registered with us.'; +      } +    } +    return 'There was a problem communicating with your device.'; +  } +} diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 3a2534d553b..cc3f02e75f6 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,98 +1,89 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */ +/* eslint-disable func-names, wrap-iife */  /* global u2f */ -/* global U2FError */ -/* global U2FUtil */  import _ from 'underscore'; +import isU2FSupported from './util'; +import U2FError from './error';  // Register U2F (universal 2nd factor) devices for users to authenticate with.  //  // State Flow #1: setup -> in_progress -> registered -> POST to server  // State Flow #2: setup -> in_progress -> error -> setup -(function() { -  this.U2FRegister = (function() { -    function U2FRegister(container, u2fParams) { -      this.container = container; -      this.renderNotSupported = this.renderNotSupported.bind(this); -      this.renderRegistered = this.renderRegistered.bind(this); -      this.renderError = this.renderError.bind(this); -      this.renderInProgress = this.renderInProgress.bind(this); -      this.renderSetup = this.renderSetup.bind(this); -      this.renderTemplate = this.renderTemplate.bind(this); -      this.register = this.register.bind(this); -      this.start = this.start.bind(this); -      this.appId = u2fParams.app_id; -      this.registerRequests = u2fParams.register_requests; -      this.signRequests = u2fParams.sign_requests; -    } +export default class U2FRegister { +  constructor(container, u2fParams) { +    this.container = container; +    this.renderNotSupported = this.renderNotSupported.bind(this); +    this.renderRegistered = this.renderRegistered.bind(this); +    this.renderError = this.renderError.bind(this); +    this.renderInProgress = this.renderInProgress.bind(this); +    this.renderSetup = this.renderSetup.bind(this); +    this.renderTemplate = this.renderTemplate.bind(this); +    this.register = this.register.bind(this); +    this.start = this.start.bind(this); +    this.appId = u2fParams.app_id; +    this.registerRequests = u2fParams.register_requests; +    this.signRequests = u2fParams.sign_requests; -    U2FRegister.prototype.start = function() { -      if (U2FUtil.isU2FSupported()) { -        return this.renderSetup(); -      } else { -        return this.renderNotSupported(); -      } +    this.templates = { +      notSupported: '#js-register-u2f-not-supported', +      setup: '#js-register-u2f-setup', +      inProgress: '#js-register-u2f-in-progress', +      error: '#js-register-u2f-error', +      registered: '#js-register-u2f-registered',      }; +  } -    U2FRegister.prototype.register = function() { -      return u2f.register(this.appId, this.registerRequests, this.signRequests, (function(_this) { -        return function(response) { -          var error; -          if (response.errorCode) { -            error = new U2FError(response.errorCode, 'register'); -            return _this.renderError(error); -          } else { -            return _this.renderRegistered(JSON.stringify(response)); -          } -        }; -      })(this), 10); -    }; +  start() { +    if (isU2FSupported()) { +      return this.renderSetup(); +    } +    return this.renderNotSupported(); +  } -    // Rendering # -    U2FRegister.prototype.templates = { -      "notSupported": "#js-register-u2f-not-supported", -      "setup": '#js-register-u2f-setup', -      "inProgress": '#js-register-u2f-in-progress', -      "error": '#js-register-u2f-error', -      "registered": '#js-register-u2f-registered' -    }; +  register() { +    return u2f.register(this.appId, this.registerRequests, this.signRequests, (function (_this) { +      return function (response) { +        if (response.errorCode) { +          const error = new U2FError(response.errorCode, 'register'); +          return _this.renderError(error); +        } +        return _this.renderRegistered(JSON.stringify(response)); +      }; +    })(this), 10); +  } -    U2FRegister.prototype.renderTemplate = function(name, params) { -      var template, templateString; -      templateString = $(this.templates[name]).html(); -      template = _.template(templateString); -      return this.container.html(template(params)); -    }; +  renderTemplate(name, params) { +    const templateString = $(this.templates[name]).html(); +    const template = _.template(templateString); +    return this.container.html(template(params)); +  } -    U2FRegister.prototype.renderSetup = function() { -      this.renderTemplate('setup'); -      return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress); -    }; +  renderSetup() { +    this.renderTemplate('setup'); +    return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress); +  } -    U2FRegister.prototype.renderInProgress = function() { -      this.renderTemplate('inProgress'); -      return this.register(); -    }; +  renderInProgress() { +    this.renderTemplate('inProgress'); +    return this.register(); +  } -    U2FRegister.prototype.renderError = function(error) { -      this.renderTemplate('error', { -        error_message: error.message(), -        error_code: error.errorCode -      }); -      return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); -    }; +  renderError(error) { +    this.renderTemplate('error', { +      error_message: error.message(), +      error_code: error.errorCode, +    }); +    return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); +  } -    U2FRegister.prototype.renderRegistered = function(deviceResponse) { -      this.renderTemplate('registered'); -      // Prefer to do this instead of interpolating using Underscore templates -      // because of JSON escaping issues. -      return this.container.find("#js-device-response").val(deviceResponse); -    }; - -    U2FRegister.prototype.renderNotSupported = function() { -      return this.renderTemplate('notSupported'); -    }; +  renderRegistered(deviceResponse) { +    this.renderTemplate('registered'); +    // Prefer to do this instead of interpolating using Underscore templates +    // because of JSON escaping issues. +    return this.container.find('#js-device-response').val(deviceResponse); +  } -    return U2FRegister; -  })(); -}).call(window); +  renderNotSupported() { +    return this.renderTemplate('notSupported'); +  } +} diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js index 813d363db00..9771ff935c2 100644 --- a/app/assets/javascripts/u2f/util.js +++ b/app/assets/javascripts/u2f/util.js @@ -1,12 +1,3 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife */ -(function() { -  this.U2FUtil = (function() { -    function U2FUtil() {} - -    U2FUtil.isU2FSupported = function() { -      return window.u2f; -    }; - -    return U2FUtil; -  })(); -}).call(window); +export default function isU2FSupported() { +  return window.u2f; +} diff --git a/app/assets/javascripts/users/index.js b/app/assets/javascripts/users/index.js index 33a83f8dae5..9fd8452a2b6 100644 --- a/app/assets/javascripts/users/index.js +++ b/app/assets/javascripts/users/index.js @@ -1,7 +1,7 @@  import Cookies from 'js-cookie';  import UserTabs from './user_tabs'; -export default function initUserProfile(action) { +function initUserProfile(action) {    // place profile avatars to top    $('.profile-groups-avatars').tooltip({      placement: 'top', @@ -17,3 +17,9 @@ export default function initUserProfile(action) {      $(this).parents('.project-limit-message').remove();    });  } + +document.addEventListener('DOMContentLoaded', () => { +  const page = $('body').attr('data-page'); +  const action = page.split(':')[1]; +  initUserProfile(action); +}); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 73676bd6de7..759cc9925f4 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -6,7 +6,7 @@ import _ from 'underscore';  // TODO: remove eventHub hack after code splitting refactor  window.emitSidebarEvent = window.emitSidebarEvent || $.noop; -function UsersSelect(currentUser, els) { +function UsersSelect(currentUser, els, options = {}) {    var $els;    this.users = this.users.bind(this);    this.user = this.user.bind(this); @@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) {      }    } +  const { handleClick } = options; +    $els = $(els);    if (!els) { @@ -424,7 +426,7 @@ function UsersSelect(currentUser, els) {            }            var isIssueIndex, isMRIndex, page, selected; -          page = $('body').data('page'); +          page = $('body').attr('data-page');            isIssueIndex = page === 'projects:issues:index';            isMRIndex = (page === page && page === 'projects:merge_requests:index');            if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { @@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) {            }            if ($el.closest('.add-issues-modal').length) {              gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; +          } else if (handleClick) { +            e.preventDefault(); +            handleClick(user, isMarking);            } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {              return Issuable.filterResults($dropdown.closest('form'));            } else if ($dropdown.hasClass('js-filter-submit')) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index e98d147733c..e86a0f7e749 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -1,6 +1,5 @@ -/* global Flash */ -  import '~/lib/utils/datetime_utility'; +import Flash from '../../flash';  import MemoryUsage from './mr_widget_memory_usage';  import StatusIcon from './mr_widget_status_icon';  import MRWidgetService from '../services/mr_widget_service'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js index c79b5c720eb..029832bdd27 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js @@ -1,6 +1,6 @@  import PipelineStage from '../../pipelines/components/stage.vue';  import ciIcon from '../../vue_shared/components/ci_icon.vue'; -import { statusIconEntityMap } from '../../vue_shared/ci_status_icons'; +import icon from '../../vue_shared/components/icon.vue';  export default {    name: 'MRWidgetPipeline', @@ -10,6 +10,7 @@ export default {    components: {      'pipeline-stage': PipelineStage,      ciIcon, +    icon,    },    computed: {      hasPipeline() { @@ -20,9 +21,6 @@ export default {        return hasCI && !ciStatus;      }, -    svg() { -      return statusIconEntityMap.icon_status_failed; -    },      stageText() {        return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';      }, @@ -38,8 +36,10 @@ export default {          <template v-if="hasCIError">            <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">              <span -              v-html="svg" -              aria-hidden="true"></span> +              aria-hidden="true"> +              <icon +                name="status_failed"/> +            </span>            </div>            <div class="media-body">              Could not connect to the CI server. Please check your settings and try again diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js index bdfd4d9667c..05c4a28be88 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js @@ -1,4 +1,4 @@ -/* global Flash */ +import Flash from '../../../flash';  import statusIcon from '../mr_widget_status_icon';  import MRWidgetAuthor from '../../components/mr_widget_author';  import eventHub from '../../event_hub'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js index 74fc52796a0..2dfd87ed904 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js @@ -1,5 +1,4 @@ -/* global Flash */ - +import Flash from '../../../flash';  import mrWidgetAuthorTime from '../../components/mr_widget_author_time';  import tooltip from '../../../vue_shared/directives/tooltip';  import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index 61734163b6e..be37dd87de9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -1,7 +1,7 @@ -/* global Flash */  import successSvg from 'icons/_icon_status_success.svg';  import warningSvg from 'icons/_icon_status_warning.svg';  import simplePoll from '~/lib/utils/simple_poll'; +import Flash from '../../../flash';  import statusIcon from '../mr_widget_status_icon';  import eventHub from '../../event_hub'; @@ -286,6 +286,7 @@ export default {                  <input                    id="remove-source-branch-input"                    v-model="removeSourceBranch" +                  class="js-remove-source-branch-checkbox"                    :disabled="isRemoveSourceBranchButtonDisabled"                    type="checkbox"/> Remove source branch                </label> @@ -311,8 +312,8 @@ export default {                </button>              </template>              <template v-else> -              <span class="bold"> -                The pipeline for this merge request has not succeeded yet +              <span class="bold js-resolve-mr-widget-items-message"> +                You can only merge once the items above are resolved                </span>              </template>            </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js index 54be1fbe675..4f83350e07c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js @@ -1,4 +1,3 @@ -/* global Flash */  import statusIcon from '../mr_widget_status_icon';  import tooltip from '../../../vue_shared/directives/tooltip';  import eventHub from '../../event_hub'; @@ -27,12 +26,12 @@ export default {          .then(res => res.json())          .then((res) => {            eventHub.$emit('UpdateWidgetData', res); -          new Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line +          new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line            $('.merge-request .detail-page-description .title').text(this.mr.title);          })          .catch(() => {            this.isMakingRequest = false; -          new Flash('Something went wrong. Please try again.'); // eslint-disable-line +          new window.Flash('Something went wrong. Please try again.'); // eslint-disable-line          });      },    }, diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index 044b664484b..aaff9ee6518 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -1,5 +1,5 @@ -/* global Flash */ - +import SmartInterval from '~/smart_interval'; +import Flash from '../flash';  import {    WidgetHeader,    WidgetMergeHelp, @@ -82,7 +82,7 @@ export default {        return new MRWidgetService(endpoints);      },      checkStatus(cb) { -      this.service.checkStatus() +      return this.service.checkStatus()          .then(res => res.json())          .then((res) => {            this.handleNotification(res); @@ -98,7 +98,7 @@ export default {          });      },      initPolling() { -      this.pollingInterval = new gl.SmartInterval({ +      this.pollingInterval = new SmartInterval({          callback: this.checkStatus,          startingInterval: 10000,          maxInterval: 30000, @@ -107,7 +107,7 @@ export default {        });      },      initDeploymentsPolling() { -      this.deploymentsInterval = new gl.SmartInterval({ +      this.deploymentsInterval = new SmartInterval({          callback: this.fetchDeployments,          startingInterval: 30000,          maxInterval: 120000, @@ -122,7 +122,7 @@ export default {        }      },      fetchDeployments() { -      this.service.fetchDeployments() +      return this.service.fetchDeployments()          .then(res => res.json())          .then((res) => {            if (res.length) { diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js index 79c3d335679..99f5c305df5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js +++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js @@ -11,7 +11,7 @@ export default class MRWidgetService {      this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);      this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);      this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath); -    this.pollResource = Vue.resource(`${endpoints.statusPath}?basic=true`); +    this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`);      this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);    } diff --git a/app/assets/javascripts/vue_shared/ci_action_icons.js b/app/assets/javascripts/vue_shared/ci_action_icons.js deleted file mode 100644 index b21f0ab49fd..00000000000 --- a/app/assets/javascripts/vue_shared/ci_action_icons.js +++ /dev/null @@ -1,21 +0,0 @@ -import cancelSVG from 'icons/_icon_action_cancel.svg'; -import retrySVG from 'icons/_icon_action_retry.svg'; -import playSVG from 'icons/_icon_action_play.svg'; -import stopSVG from 'icons/_icon_action_stop.svg'; - -/** - * For the provided action returns the respective SVG - * - * @param  {String} action - * @return {SVG|String} - */ -export default function getActionIcon(action) { -  const icons = { -    icon_action_cancel: cancelSVG, -    icon_action_play: playSVG, -    icon_action_retry: retrySVG, -    icon_action_stop: stopSVG, -  }; - -  return icons[action] || ''; -} diff --git a/app/assets/javascripts/vue_shared/ci_status_icons.js b/app/assets/javascripts/vue_shared/ci_status_icons.js deleted file mode 100644 index d9d0cad38e4..00000000000 --- a/app/assets/javascripts/vue_shared/ci_status_icons.js +++ /dev/null @@ -1,43 +0,0 @@ -import BORDERLESS_CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg'; -import BORDERLESS_CREATED_SVG from 'icons/_icon_status_created_borderless.svg'; -import BORDERLESS_FAILED_SVG from 'icons/_icon_status_failed_borderless.svg'; -import BORDERLESS_MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg'; -import BORDERLESS_PENDING_SVG from 'icons/_icon_status_pending_borderless.svg'; -import BORDERLESS_RUNNING_SVG from 'icons/_icon_status_running_borderless.svg'; -import BORDERLESS_SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg'; -import BORDERLESS_SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg'; -import BORDERLESS_WARNING_SVG from 'icons/_icon_status_warning_borderless.svg'; - -import CANCELED_SVG from 'icons/_icon_status_canceled.svg'; -import CREATED_SVG from 'icons/_icon_status_created.svg'; -import FAILED_SVG from 'icons/_icon_status_failed.svg'; -import MANUAL_SVG from 'icons/_icon_status_manual.svg'; -import PENDING_SVG from 'icons/_icon_status_pending.svg'; -import RUNNING_SVG from 'icons/_icon_status_running.svg'; -import SKIPPED_SVG from 'icons/_icon_status_skipped.svg'; -import SUCCESS_SVG from 'icons/_icon_status_success.svg'; -import WARNING_SVG from 'icons/_icon_status_warning.svg'; - -export const borderlessStatusIconEntityMap = { -  icon_status_canceled: BORDERLESS_CANCELED_SVG, -  icon_status_created: BORDERLESS_CREATED_SVG, -  icon_status_failed: BORDERLESS_FAILED_SVG, -  icon_status_manual: BORDERLESS_MANUAL_SVG, -  icon_status_pending: BORDERLESS_PENDING_SVG, -  icon_status_running: BORDERLESS_RUNNING_SVG, -  icon_status_skipped: BORDERLESS_SKIPPED_SVG, -  icon_status_success: BORDERLESS_SUCCESS_SVG, -  icon_status_warning: BORDERLESS_WARNING_SVG, -}; - -export const statusIconEntityMap = { -  icon_status_canceled: CANCELED_SVG, -  icon_status_created: CREATED_SVG, -  icon_status_failed: FAILED_SVG, -  icon_status_manual: MANUAL_SVG, -  icon_status_pending: PENDING_SVG, -  icon_status_running: RUNNING_SVG, -  icon_status_skipped: SKIPPED_SVG, -  icon_status_success: SUCCESS_SVG, -  icon_status_warning: WARNING_SVG, -}; diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index caa28bff6db..fc795936abf 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -1,52 +1,63 @@  <script> -import ciIcon from './ci_icon.vue'; -/** - * Renders CI Badge link with CI icon and status text based on - * API response shared between all places where it is used. - * - * Receives status object containing: - * status: { - *   details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url - *   group:"running" // used for CSS class - *   icon: "icon_status_running" // used to render the icon - *   label:"running" // used for potential tooltip - *   text:"running" // text rendered - * } - * - * Used in: - * - Pipelines table - first column - * - Jobs table - first column - * - Pipeline show view - header - * - Job show view - header - * - MR widget - */ +  import ciIcon from './ci_icon.vue'; +  import tooltip from '../directives/tooltip'; +  /** +   * Renders CI Badge link with CI icon and status text based on +   * API response shared between all places where it is used. +   * +   * Receives status object containing: +   * status: { +   *   details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url +   *   group:"running" // used for CSS class +   *   icon: "icon_status_running" // used to render the icon +   *   label:"running" // used for potential tooltip +   *   text:"running" // text rendered +   * } +   * +   * Used in: +   * - Pipelines table - first column +   * - Jobs table - first column +   * - Pipeline show view - header +   * - Job show view - header +   * - MR widget +   */ -export default { -  props: { -    status: { -      type: Object, -      required: true, +  export default { +    props: { +      status: { +        type: Object, +        required: true, +      }, +      showText: { +        type: Boolean, +        required: false, +        default: true, +      },      }, -  }, - -  components: { -    ciIcon, -  }, - -  computed: { -    cssClass() { -      const className = this.status.group; - -      return className ? `ci-status ci-${this.status.group}` : 'ci-status'; +    components: { +      ciIcon,      }, -  }, -}; +    directives: { +      tooltip, +    }, +    computed: { +      cssClass() { +        const className = this.status.group; +        return className ? `ci-status ci-${className}` : 'ci-status'; +      }, +    }, +  };  </script>  <template>    <a      :href="status.details_path" -    :class="cssClass"> +    :class="cssClass" +    v-tooltip +    :title="!showText ? status.text : ''">      <ci-icon :status="status" /> -    {{status.text}} + +    <template v-if="showText"> +      {{status.text}} +    </template>    </a>  </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index ec88119e16c..2a018f38366 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -1,5 +1,5 @@  <script> -  import { statusIconEntityMap } from '../ci_status_icons'; +  import icon from '../../vue_shared/components/icon.vue';    /**     * Renders CI icon based on API response shared between all places where it is used. @@ -30,11 +30,11 @@        },      }, -    computed: { -      statusIconSvg() { -        return statusIconEntityMap[this.status.icon]; -      }, +    components: { +      icon, +    }, +    computed: {        cssClass() {          const status = this.status.group;          return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; @@ -44,7 +44,8 @@  </script>  <template>    <span -    :class="cssClass" -    v-html="statusIconSvg"> +    :class="cssClass"> +    <icon +      :name="status.icon"/>    </span>  </template> diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 50d14282cad..52814de8b2d 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -63,14 +63,17 @@          required: false,          default: () => ({}),        }, +      showBranch: { +        type: Boolean, +        required: false, +        default: true, +      },      },      computed: {        /**         * Used to verify if all the properties needed to render the commit         * ref section were provided.         * -       * TODO: Improve this! Use lodash _.has when we have it. -       *         * @returns {Boolean}         */        hasCommitRef() { @@ -80,8 +83,6 @@         * Used to verify if all the properties needed to render the commit         * author section were provided.         * -       * TODO: Improve this! Use lodash _.has when we have it. -       *         * @returns {Boolean}         */        hasAuthor() { @@ -114,31 +115,30 @@  </script>  <template>    <div class="branch-commit"> -    <div -      v-if="hasCommitRef" -      class="icon-container hidden-xs"> -      <i -        v-if="tag" -        class="fa fa-tag" -        aria-hidden="true"> -      </i> -      <i -        v-if="!tag" -        class="fa fa-code-fork" -        aria-hidden="true"> -      </i> -    </div> - -    <a -      v-if="hasCommitRef" -      class="ref-name hidden-xs" -      :href="commitRef.ref_url" -      v-tooltip -      data-container="body" -      :title="commitRef.name"> -      {{commitRef.name}} -    </a> +    <template v-if="hasCommitRef && showBranch"> +      <div +        class="icon-container hidden-xs"> +        <i +          v-if="tag" +          class="fa fa-tag" +          aria-hidden="true"> +        </i> +        <i +          v-if="!tag" +          class="fa fa-code-fork" +          aria-hidden="true"> +        </i> +      </div> +      <a +        class="ref-name hidden-xs" +        :href="commitRef.ref_url" +        v-tooltip +        data-container="body" +        :title="commitRef.name"> +        {{commitRef.name}} +      </a> +    </template>      <div        v-html="commitIconSvg"        class="commit-icon js-commit-icon"> diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue new file mode 100644 index 00000000000..2e5f9f1088f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -0,0 +1,52 @@ +<script> + +/* This is a re-usable vue component for rendering a svg sprite +   icon + +  Sample configuration: + +  <icon +    :img-src="userAvatarSrc" +    :img-alt="tooltipText" +    :tooltip-text="tooltipText" +    tooltip-placement="top" +  /> + +*/ +  export default { +    props: { +      name: { +        type: String, +        required: true, +      }, + +      size: { +        type: Number, +        required: false, +        default: 0, +      }, + +      cssClasses: { +        type: String, +        required: false, +        default: '', +      }, +    }, + +    computed: { +      spriteHref() { +        return `${gon.sprite_icons}#${this.name}`; +      }, +      iconSizeClass() { +        return this.size ? `s${this.size}` : ''; +      }, +    }, +  }; +</script> +<template> +  <svg +    :class="[iconSizeClass, cssClasses]"> +    <use  +      v-bind="{'xlink:href':spriteHref}"/> +  </svg> +</template> diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue new file mode 100644 index 00000000000..0cc2653761c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -0,0 +1,76 @@ +<script> + +/* This is a re-usable vue component for rendering a button +   that will probably be sending off ajax requests and need +   to show the loading status by setting the `loading` option. +   This can also be used for initial page load when you don't +   know the action of the button yet by setting +   `loading: true, label: undefined`. + +  Sample configuration: + +  <loading-button +    :loading="true" +    :label="Hello" +    @click="..." +  /> + +*/ + +import loadingIcon from './loading_icon.vue'; + +export default { +  props: { +    loading: { +      type: Boolean, +      required: false, +      default: false, +    }, +    disabled: { +      type: Boolean, +      required: false, +      default: false, +    }, +    label: { +      type: String, +      required: false, +    }, +  }, +  components: { +    loadingIcon, +  }, +  methods: { +    onClick(e) { +      this.$emit('click', e); +    }, +  }, +}; +</script> + +<template> +  <button +    class="btn btn-align-content" +    @click="onClick" +    type="button" +    :disabled="loading || disabled" +  > +      <transition name="fade"> +        <loading-icon +          v-if="loading" +          :inline="true" +          class="js-loading-button-icon" +          :class="{ +              'append-right-5': label +          }" +        /> +      </transition> +      <transition name="fade"> +        <span +          v-if="label" +          class="js-loading-button-label" +        > +          {{ label }} +        </span> +      </transition> +  </button> +</template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 759d30c9c7c..a873e00d0f3 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -1,7 +1,9 @@  <script> -  /* global Flash */ +  import Flash from '../../../flash'; +  import GLForm from '../../../gl_form';    import markdownHeader from './header.vue';    import markdownToolbar from './toolbar.vue'; +  import icon from '../icon.vue';    export default {      props: { @@ -36,6 +38,7 @@      components: {        markdownHeader,        markdownToolbar, +      icon,      },      computed: {        shouldShowReferencedUsers() { @@ -44,8 +47,10 @@        },      },      methods: { -      toggleMarkdownPreview() { -        this.previewMarkdown = !this.previewMarkdown; +      showPreviewTab() { +        if (this.previewMarkdown) return; + +        this.previewMarkdown = true;          /*            Can't use `$refs` as the component is technically in the parent component @@ -53,20 +58,22 @@          */          const text = this.$slots.textarea[0].elm.value; -        if (!this.previewMarkdown) { -          this.markdownPreview = ''; -        } else if (text) { +        if (text) {            this.markdownPreviewLoading = true;            this.$http.post(this.markdownPreviewPath, { text })              .then(resp => resp.json()) -            .then((data) => { -              this.renderMarkdown(data); -            }) +            .then(data => this.renderMarkdown(data))              .catch(() => new Flash('Error loading markdown preview'));          } else {            this.renderMarkdown();          }        }, + +      showWriteTab() { +        this.markdownPreview = ''; +        this.previewMarkdown = false; +      }, +        renderMarkdown(data = {}) {          this.markdownPreviewLoading = false;          this.markdownPreview = data.body || 'Nothing to preview.'; @@ -85,7 +92,7 @@        /*          GLForm class handles all the toolbar buttons        */ -      return new gl.GLForm($(this.$refs['gl-form']), true); +      return new GLForm($(this.$refs['gl-form']), true);      },      beforeDestroy() {        const glForm = $(this.$refs['gl-form']).data('gl-form'); @@ -103,7 +110,8 @@      ref="gl-form">      <markdown-header        :preview-markdown="previewMarkdown" -      @toggle-markdown="toggleMarkdownPreview" /> +      @preview-markdown="showPreviewTab" +      @write-markdown="showWriteTab" />      <div        class="md-write-holder"        v-show="!previewMarkdown"> @@ -113,10 +121,10 @@            class="zen-control zen-control-leave js-zen-leave"            href="#"            aria-label="Enter zen mode"> -          <i -            class="fa fa-compress" -            aria-hidden="true"> -          </i> +          <icon +            name="screen-normal" +            :size="32"> +          </icon>          </a>          <markdown-toolbar            :markdown-docs-path="markdownDocsPath" diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 5bf2a90cc3b..70f5fc1d664 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,6 +1,7 @@  <script>    import tooltip from '../../directives/tooltip';    import toolbarButton from './toolbar_button.vue'; +  import icon from '../icon.vue';    export default {      props: { @@ -14,25 +15,34 @@      },      components: {        toolbarButton, +      icon,      },      methods: { -      toggleMarkdownPreview(e, form) { -        if (form && !form.find('.js-vue-markdown-field').length) { -          return; -        } else if (e.target.blur) { -          e.target.blur(); -        } +      isMarkdownForm(form) { +        return form && !form.find('.js-vue-markdown-field').length; +      }, + +      previewMarkdownTab(event, form) { +        if (event.target.blur) event.target.blur(); +        if (this.isMarkdownForm(form)) return; + +        this.$emit('preview-markdown'); +      }, + +      writeMarkdownTab(event, form) { +        if (event.target.blur) event.target.blur(); +        if (this.isMarkdownForm(form)) return; -        this.$emit('toggle-markdown'); +        this.$emit('write-markdown');        },      },      mounted() { -      $(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview); -      $(document).on('markdown-preview:hide.vue', this.toggleMarkdownPreview); +      $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); +      $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);      },      beforeDestroy() { -      $(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview); -      $(document).off('markdown-preview:hide.vue', this.toggleMarkdownPreview); +      $(document).off('markdown-preview:show.vue', this.previewMarkdownTab); +      $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);      },    };  </script> @@ -42,17 +52,19 @@      <ul class="nav-links clearfix">        <li :class="{ active: !previewMarkdown }">          <a +          class="js-write-link"            href="#md-write-holder"            tabindex="-1" -          @click.prevent="toggleMarkdownPreview($event)"> +          @click.prevent="writeMarkdownTab($event)">            Write          </a>        </li>        <li :class="{ active: previewMarkdown }">          <a +          class="js-preview-link"            href="#md-preview-holder"            tabindex="-1" -          @click.prevent="toggleMarkdownPreview($event)"> +          @click.prevent="previewMarkdownTab($event)">            Preview          </a>        </li> @@ -70,7 +82,7 @@              tag="> "              :prepend="true"              button-title="Insert a quote" -            icon="quote-right" /> +            icon="quote" />            <toolbar-button              tag="`"              tag-block="```" @@ -80,17 +92,17 @@              tag="* "              :prepend="true"              button-title="Add a bullet list" -            icon="list-ul" /> +            icon="list-bulleted" />            <toolbar-button              tag="1. "              :prepend="true"              button-title="Add a numbered list" -            icon="list-ol" /> +            icon="list-numbered" />            <toolbar-button              tag="* [ ] "              :prepend="true"              button-title="Add a task list" -            icon="check-square-o" /> +            icon="task-done" />          </div>          <div class="toolbar-group">            <button @@ -101,10 +113,9 @@              tabindex="-1"              title="Go full screen"              type="button"> -            <i -              aria-hidden="true" -              class="fa fa-arrows-alt fa-fw"> -            </i> +            <icon +              name="screen-full"> +            </icon>            </button>          </div>        </li> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index f7da7ebfcfe..b930fb116a3 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -1,5 +1,6 @@  <script>    import tooltip from '../../directives/tooltip'; +  import icon from '../icon.vue';    export default {      props: { @@ -26,14 +27,12 @@          default: false,        },      }, +    components: { +      icon, +    },      directives: {        tooltip,      }, -    computed: { -      iconClass() { -        return `fa-${this.icon}`; -      }, -    },    };  </script> @@ -49,10 +48,8 @@      :data-md-prepend="prepend"      :title="buttonTitle"      :aria-label="buttonTitle"> -    <i -      aria-hidden="true" -      class="fa fa-fw" -      :class="iconClass"> -    </i> +    <icon +      :name="icon"> +    </icon>    </button>  </template> diff --git a/app/assets/javascripts/notes/components/issue_placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index 6921d91372f..e467ca56704 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -1,9 +1,26 @@  <script> +  /** +   * Common component to render a placeholder note and user information. +   * +   * This component needs to be used with a vuex store. +   * That vuex store needs to have a `getUserData` getter that contains +   * { +   *   path: String, +   *   avatar_url: String, +   *   name: String, +   *   username: String, +   * } +   * +   * @example +   * <placeholder-note +   *   :note="{body: 'This is a note'}" +   *   /> +   */    import { mapGetters } from 'vuex'; -  import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +  import userAvatarLink from '../user_avatar/user_avatar_link.vue';    export default { -    name: 'issuePlaceholderNote', +    name: 'placeholderNote',      props: {        note: {          type: Object, diff --git a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue index 80a8ef56a83..d805fea8006 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue @@ -1,4 +1,12 @@  <script> +  /** +   * Common component to render a placeholder system note. +   * +   * @example +   * <placeholder-system-note +   *  :note="{ body: 'Commands are being applied'}" +   *  /> +   */    export default {      name: 'placeholderSystemNote',      props: { diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 0cfb6522e77..98f8f32557d 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -1,6 +1,24 @@  <script> +  /** +   * Common component to render a system note, icon and user information. +   * +   * This component needs to be used with a vuex store. +   * That vuex store needs to have a `targetNoteHash` getter +   * +   * @example +   * <system-note +   *   :note="{ +   *     id: String, +   *     author: Object, +   *     createdAt: String, +   *     note_html: String, +   *     system_note_icon_name: String +   *    }" +   *   /> +   */    import { mapGetters } from 'vuex'; -  import issueNoteHeader from './issue_note_header.vue'; +  import issueNoteHeader from '../../../notes/components/issue_note_header.vue'; +  import { spriteIcon } from '../../../lib/utils/common_utils';    export default {      name: 'systemNote', @@ -24,7 +42,7 @@          return this.targetNoteHash === this.noteAnchorId;        },        iconHtml() { -        return gl.utils.spriteIcon(this.note.system_note_icon_name); +        return spriteIcon(this.note.system_note_icon_name);        },      },    }; @@ -46,7 +64,8 @@              :author="note.author"              :created-at="note.created_at"              :note-id="note.id" -            :action-text-html="note.note_html" /> +            :action-text-html="note.note_html" +            />          </div>        </div>      </div> diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index 9279b50cd55..47efee64c6e 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -5,17 +5,32 @@ export default {    props: {      title: {        type: String, -      required: true, +      required: false,      },      text: {        type: String, -      required: true, +      required: false, +    }, +    hideFooter: { +      type: Boolean, +      required: false, +      default: false,      },      kind: {        type: String,        required: false,        default: 'primary',      }, +    modalDialogClass: { +      type: String, +      required: false, +      default: '', +    }, +    closeKind: { +      type: String, +      required: false, +      default: 'default', +    },      closeButtonLabel: {        type: String,        required: false, @@ -25,6 +40,11 @@ export default {        type: String,        required: true,      }, +    submitDisabled: { +      type: Boolean, +      required: false, +      default: false, +    },    },    computed: { @@ -33,6 +53,11 @@ export default {          [`btn-${this.kind}`]: true,        };      }, +    btnCancelKindClass() { +      return { +        [`btn-${this.closeKind}`]: true, +      }; +    },    },    methods: { @@ -47,41 +72,58 @@ export default {  </script>  <template> -<div -  class="modal popup-dialog" -  role="dialog" -  tabindex="-1"> -  <div class="modal-dialog" role="document"> -    <div class="modal-content"> -      <div class="modal-header"> -        <button type="button" -          class="close" -          @click="close" -          aria-label="Close"> -          <span aria-hidden="true">×</span> -        </button> -        <h4 class="modal-title">{{this.title}}</h4> -      </div> -      <div class="modal-body"> -        <slot name="body" :text="text"> -          <p>{{text}}</p> -        </slot> -      </div> -      <div class="modal-footer"> -        <button -          type="button" -          class="btn btn-default" -          @click="emitSubmit(false)"> -            {{closeButtonLabel}} -        </button> -        <button type="button" -          class="btn" -          :class="btnKindClass" -          @click="emitSubmit(true)"> -            {{primaryButtonLabel}} -        </button> +<div class="modal-open"> +  <div +    class="modal popup-dialog" +    role="dialog" +    tabindex="-1" +  > +    <div +      :class="modalDialogClass" +      class="modal-dialog" +      role="document" +    > +      <div class="modal-content"> +        <div class="modal-header"> +          <slot name="header"> +            <h4 class="modal-title pull-left"> +              {{this.title}} +            </h4> +            <button +              type="button" +              class="close pull-right" +              @click="close" +              aria-label="Close" +            > +              <span aria-hidden="true">×</span> +            </button> +          </slot> +        </div> +        <div class="modal-body"> +          <slot name="body" :text="text"> +            <p>{{this.text}}</p> +          </slot> +        </div> +        <div class="modal-footer" v-if="!hideFooter"> +          <button +            type="button" +            class="btn pull-left" +            :class="btnCancelKindClass" +            @click="close"> +            {{ closeButtonLabel }} +          </button> +          <button +            type="button" +            class="btn pull-right" +            :disabled="submitDisabled" +            :class="btnKindClass" +            @click="emitSubmit(true)"> +            {{ primaryButtonLabel }} +          </button> +        </div>        </div>      </div>    </div> +  <div class="modal-backdrop fade in" />  </div>  </template> diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue new file mode 100644 index 00000000000..b06493e6c66 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue @@ -0,0 +1,37 @@ +<script> +  export default { +    props: { +      small: { +        type: Boolean, +        required: false, +        default: false, +      }, +      lines: { +        type: Number, +        required: false, +        default: 6, +      }, +    }, +    computed: { +      lineClasses() { +        return new Array(this.lines).fill().map((_, i) => `skeleton-line-${i + 1}`); +      }, +    }, +  }; +</script> + +<template> +  <div +    class="animation-container" +    :class="{ +      'animation-container-small': small, +    }" +  > +    <div +      v-for="(css, index) in lineClasses" +      :key="index" +      :class="css" +    > +    </div> +  </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index dd9a2ebb184..1ac61a3c39b 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -7,6 +7,7 @@    Sample configuration:    <user-avatar-image +    :lazy="true"      :img-src="userAvatarSrc"      :img-alt="tooltipText"      :tooltip-text="tooltipText" @@ -16,11 +17,17 @@  */  import defaultAvatarUrl from 'images/no_avatar.png'; +import { placeholderImage } from '../../../lazy_loader';  import tooltip from '../../directives/tooltip';  export default {    name: 'UserAvatarImage',    props: { +    lazy: { +      type: Boolean, +      required: false, +      default: false, +    },      imgSrc: {        type: String,        required: false, @@ -56,18 +63,21 @@ export default {      tooltip,    },    computed: { +    // API response sends null when gravatar is disabled and +    // we provide an empty string when we use it inside user avatar link. +    // In both cases we should render the defaultAvatarUrl +    sanitizedSource() { +      return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc; +    }, +    resultantSrcAttribute() { +      return this.lazy ? placeholderImage : this.sanitizedSource; +    },      tooltipContainer() {        return this.tooltipText ? 'body' : null;      },      avatarSizeClass() {        return `s${this.size}`;      }, -    // API response sends null when gravatar is disabled and -    // we provide an empty string when we use it inside user avatar link. -    // In both cases we should render the defaultAvatarUrl -    imageSource() { -      return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc; -    },    },  };  </script> @@ -76,11 +86,16 @@ export default {    <img      v-tooltip      class="avatar" -    :class="[avatarSizeClass, cssClasses]" -    :src="imageSource" +    :class="{ +      lazy, +      [avatarSizeClass]: true, +      [cssClasses]: true +    }" +    :src="resultantSrcAttribute"      :width="size"      :height="size"      :alt="imgAlt" +    :data-src="sanitizedSource"      :data-container="tooltipContainer"      :data-placement="tooltipPlacement"      :title="tooltipText" diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index 95898d54cf7..dc32e783258 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -12,12 +12,14 @@      :img-alt="tooltipText"      :img-size="20"      :tooltip-text="tooltipText" -    tooltip-placement="top" +    :tooltip-placement="top" +    :username="username"    />  */  import userAvatarImage from './user_avatar_image.vue'; +import tooltip from '../../directives/tooltip';  export default {    name: 'UserAvatarLink', @@ -60,6 +62,22 @@ export default {        required: false,        default: 'top',      }, +    username: { +      type: String, +      required: false, +      default: '', +    }, +  }, +  computed: { +    shouldShowUsername() { +      return this.username.length > 0; +    }, +    avatarTooltipText() { +      return this.shouldShowUsername ? '' : this.tooltipText; +    }, +  }, +  directives: { +    tooltip,    },  };  </script> @@ -73,8 +91,13 @@ export default {        :img-alt="imgAlt"        :css-classes="imgCssClasses"        :size="imgSize" -      :tooltip-text="tooltipText" +      :tooltip-text="avatarTooltipText" +      :tooltip-placement="tooltipPlacement" +    /><span +      v-if="shouldShowUsername" +      v-tooltip +      :title="tooltipText"        :tooltip-placement="tooltipPlacement" -    /> +    >{{username}}</span>    </a>  </template> diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 99c7644e4d9..cba7b9227cd 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -11,8 +11,6 @@ import Dropzone from 'dropzone';  import 'mousetrap';  import 'mousetrap/plugins/pause/mousetrap-pause'; -window.Dropzone = Dropzone; -  //  // ### Events  // diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c7be94e2c8e..c334f39f416 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -5,8 +5,10 @@  @import "framework/layout";  @import "framework/animations"; +@import "framework/vue_transitions";  @import "framework/avatar";  @import "framework/asciidoctor"; +@import "framework/banner";  @import "framework/blocks";  @import "framework/buttons";  @import "framework/badges"; @@ -35,9 +37,10 @@  @import "framework/secondary-navigation-elements";  @import "framework/selects";  @import "framework/sidebar"; -@import "framework/new-sidebar"; +@import "framework/contextual-sidebar";  @import "framework/tables";  @import "framework/notes"; +@import "framework/tabs";  @import "framework/timeline";  @import "framework/tooltips";  @import "framework/typography"; @@ -53,4 +56,4 @@  @import "framework/icons";  @import "framework/snippets";  @import "framework/memory_graph"; -@import "framework/responsive-tables"; +@import "framework/responsive_tables"; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index f0e6b23757f..374988bb590 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -198,6 +198,13 @@ a {      height: 12px;    } +  &.animation-container-right { +    .skeleton-line-2 { +      left: 0; +      right: 150px; +    } +  } +    &::before {      animation-duration: 1s;      animation-fill-mode: forwards; diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index f1aedc227f3..26db2386879 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -42,8 +42,7 @@    &.avatar-inline {      float: none;      display: inline-block; -    margin-left: 4px; -    margin-bottom: 2px; +    margin-left: 2px;      flex-shrink: 0;      -webkit-flex-shrink: 0; @@ -59,7 +58,7 @@    &.avatar-tile {      border-radius: 0; -    border: none; +    border: 0;    }    &:not([href]):hover { @@ -96,7 +95,7 @@    .avatar {      border-radius: 0; -    border: none; +    border: 0;      height: auto;      width: 100%;      margin: 0; diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss new file mode 100644 index 00000000000..6433b0c7855 --- /dev/null +++ b/app/assets/stylesheets/framework/banner.scss @@ -0,0 +1,25 @@ +.banner-callout { +  display: flex; +  position: relative; +  flex-wrap: wrap; + +  .banner-close { +    position: absolute; +    top: 10px; +    right: 10px; +    opacity: 1; + +    .dismiss-icon { +      color: $gl-text-color; +      font-size: $gl-font-size; +    } +  } + +  .banner-graphic { +    margin: 20px auto; +  } + +  &.banner-non-empty-state { +    border-bottom: 1px solid $border-color; +  } +} diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index dbd990f84c1..9c1439dfad5 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -39,7 +39,11 @@    }    &.top-block { -    border-top: none; +    border-top: 0; + +    .container-fluid { +      background-color: inherit; +    }    }    &.middle-block { @@ -59,7 +63,7 @@    &.footer-block {      margin-top: 0; -    border-bottom: none; +    border-bottom: 0;      margin-bottom: -$gl-padding;    } @@ -96,11 +100,7 @@    &.build-content {      background-color: $white-light; -    border-top: none; -  } - -  &.top-block .container-fluid { -    background-color: inherit; +    border-top: 0;    }  } @@ -209,7 +209,6 @@      padding: 24px 0 0;      .nav-links { -      justify-content: center;        width: 100%;        float: none; @@ -217,6 +216,14 @@          float: none;        }      } + +    li:first-child { +      margin-left: auto; +    } + +    li:last-child { +      margin-right: auto; +    }    }    .group-info { @@ -280,12 +287,12 @@      cursor: pointer;      color: $blue-300;      z-index: 1; -    border: none; +    border: 0;      background-color: transparent;      &:hover,      &:focus { -      border: none; +      border: 0;        color: $blue-400;      }    } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index b131e2d57ee..b2f26cf7159 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -292,6 +292,12 @@    }  } +.btn-align-content { +  display: flex; +  justify-content: center; +  align-items: center; +} +  .btn-group {    &.btn-grouped {      @include btn-with-margin; @@ -299,7 +305,7 @@  }  .btn-clipboard { -  border: none; +  border: 0;    padding: 0 5px;  } diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index e0e46dd73af..1bd94c0acba 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -12,15 +12,15 @@    border-left: 3px solid $border-color;    color: $text-color;    background: $gray-light; -} -.bs-callout h4 { -  margin-top: 0; -  margin-bottom: 5px; -} +  h4 { +    margin-top: 0; +    margin-bottom: 5px; +  } -.bs-callout p:last-child { -  margin-bottom: 0; +  p:last-child { +    margin-bottom: 0; +  }  }  /* Variations */ diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 96f9dda26c4..5f5b5657a2f 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -4,32 +4,9 @@  .cred { color: $common-red; }  .cgreen { color: $common-green; }  .cdark { color: $common-gray-dark; } - -/** COMMON CLASSES **/ -.prepend-top-0 { margin-top: 0; } -.prepend-top-5 { margin-top: 5px; } -.prepend-top-10 { margin-top: 10px; } -.prepend-top-default { margin-top: $gl-padding !important; } -.prepend-top-20 { margin-top: 20px; } -.prepend-left-4 { margin-left: 4px; } -.prepend-left-5 { margin-left: 5px; } -.prepend-left-10 { margin-left: 10px; } -.prepend-left-default { margin-left: $gl-padding; } -.prepend-left-20 { margin-left: 20px; } -.append-right-5 { margin-right: 5px; } -.append-right-8 { margin-right: 8px; } -.append-right-10 { margin-right: 10px; } -.append-right-default { margin-right: $gl-padding; } -.append-right-20 { margin-right: 20px; } -.append-bottom-0 { margin-bottom: 0; } -.append-bottom-5 { margin-bottom: 5px; } -.append-bottom-10 { margin-bottom: 10px; } -.append-bottom-15 { margin-bottom: 15px; } -.append-bottom-20 { margin-bottom: 20px; } -.append-bottom-default { margin-bottom: $gl-padding; } -.inline { display: inline-block; } -.center { text-align: center; } -.vertical-align-middle { vertical-align: middle; } +.text-secondary { +  color: $gl-text-color-secondary; +}  .underlined-link { text-decoration: underline; }  .hint { font-style: italic; color: $hint-color; } @@ -54,7 +31,7 @@  pre {    &.clean {      background: none; -    border: none; +    border: 0;      margin: 0;      padding: 0;    } @@ -79,6 +56,14 @@ hr {  .str-truncated {    @include str-truncated; + +  &-60 { +    @include str-truncated(60%); +  } + +  &-100 { +    @include str-truncated(100%); +  }  }  .block-truncated { @@ -104,10 +89,17 @@ hr {    font-size: 14px;  } -table a code { -  position: relative; -  top: -2px; -  margin-right: 3px; +table { +  a code { +    position: relative; +    top: -2px; +    margin-right: 3px; +  } + +  td.permission-x { +    background: $table-permission-x-bg !important; +    text-align: center; +  }  }  .loading { @@ -153,7 +145,7 @@ li.note {    img { max-width: 100%; }    .note-title {      li { -      border-bottom: none !important; +      border-bottom: 0 !important;      }    }  } @@ -198,7 +190,7 @@ li.note {    pre {      background: $white-light; -    border: none; +    border: 0;      font-size: 12px;    }  } @@ -292,13 +284,6 @@ img.emoji {    margin-bottom: 10px;  } -table { -  td.permission-x { -    background: $table-permission-x-bg !important; -    text-align: center; -  } -} -  .btn-sign-in {    text-shadow: none; @@ -364,10 +349,11 @@ table {  .dropzone .dz-preview .dz-progress {    border-color: $border-color !important; -} -.dropzone .dz-preview .dz-progress .dz-upload { -  background: $gl-success !important; +  .dz-upload { +    background: $gl-success !important; +  } +  }  .dz-message { @@ -403,7 +389,7 @@ table {  }  .hide-bottom-border { -  border-bottom: none !important; +  border-bottom: 0 !important;  }  .gl-accessibility { @@ -428,16 +414,6 @@ table {    border-radius: $border-radius-default;  } -.str-truncated { -  &-60 { -    @include str-truncated(60%); -  } - -  &-100 { -    @include str-truncated(100%); -  } -} -  .tooltip {    .tooltip-inner {      word-wrap: break-word; @@ -448,3 +424,30 @@ table {    pointer-events: none;    opacity: .5;  } + +/** COMMON CLASSES **/ +.prepend-top-0 { margin-top: 0; } +.prepend-top-5 { margin-top: 5px; } +.prepend-top-10 { margin-top: 10px; } +.prepend-top-15 { margin-top: 15px; } +.prepend-top-default { margin-top: $gl-padding !important; } +.prepend-top-20 { margin-top: 20px; } +.prepend-left-4 { margin-left: 4px; } +.prepend-left-5 { margin-left: 5px; } +.prepend-left-10 { margin-left: 10px; } +.prepend-left-default { margin-left: $gl-padding; } +.prepend-left-20 { margin-left: 20px; } +.append-right-5 { margin-right: 5px; } +.append-right-8 { margin-right: 8px; } +.append-right-10 { margin-right: 10px; } +.append-right-default { margin-right: $gl-padding; } +.append-right-20 { margin-right: 20px; } +.append-bottom-0 { margin-bottom: 0; } +.append-bottom-5 { margin-bottom: 5px; } +.append-bottom-10 { margin-bottom: 10px; } +.append-bottom-15 { margin-bottom: 15px; } +.append-bottom-20 { margin-bottom: 20px; } +.append-bottom-default { margin-bottom: $gl-padding; } +.inline { display: inline-block; } +.center { text-align: center; } +.vertical-align-middle { vertical-align: middle; } diff --git a/app/assets/stylesheets/framework/new-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss index caf4c7a40b1..320f458630a 100644 --- a/app/assets/stylesheets/framework/new-sidebar.scss +++ b/app/assets/stylesheets/framework/contextual-sidebar.scss @@ -1,24 +1,10 @@ -@import "framework/variables"; -@import 'framework/tw_bootstrap_variables'; -@import "bootstrap/variables"; - -$active-background: rgba(0, 0, 0, .04); -$active-hover-background: $active-background; -$active-hover-color: $gl-text-color; -$inactive-badge-background: rgba(0, 0, 0, .08); -$hover-background: $white-light; -$hover-color: $gl-text-color; -$inactive-color: $gl-text-color-secondary; -$new-sidebar-width: 220px; -$new-sidebar-collapsed-width: 50px; - -.page-with-new-sidebar { +.page-with-contextual-sidebar {    @media (min-width: $screen-md-min) { -    padding-left: $new-sidebar-collapsed-width; +    padding-left: $contextual-sidebar-collapsed-width;    }    @media (min-width: $screen-lg-min) { -    padding-left: $new-sidebar-width; +    padding-left: $contextual-sidebar-width;    }    // Override position: absolute @@ -34,7 +20,7 @@ $new-sidebar-collapsed-width: 50px;  .page-with-icon-sidebar {    @media (min-width: $screen-sm-min) { -    padding-left: $new-sidebar-collapsed-width; +    padding-left: $contextual-sidebar-collapsed-width;    }  } @@ -52,12 +38,12 @@ $new-sidebar-collapsed-width: 50px;    &:hover,    a:hover { -    background-color: $hover-background; -    color: $hover-color; +    background-color: $link-hover-background; +    color: $gl-text-color;      .settings-avatar {        svg { -        fill: $hover-color; +        fill: $gl-text-color;        }      }    } @@ -85,12 +71,12 @@ $new-sidebar-collapsed-width: 50px;  .nav-sidebar {    position: fixed;    z-index: 400; -  width: $new-sidebar-width; +  width: $contextual-sidebar-width;    transition: left $sidebar-transition-duration;    top: $header-height;    bottom: 0;    left: 0; -  background-color: $gray-normal; +  background-color: $gray-light;    box-shadow: inset -2px 0 0 $border-color;    transform: translate3d(0, 0, 0); @@ -103,7 +89,7 @@ $new-sidebar-collapsed-width: 50px;    &.sidebar-icons-only {      width: auto; -    min-width: $new-sidebar-collapsed-width; +    min-width: $contextual-sidebar-collapsed-width;      .nav-sidebar-inner-scroll {        overflow-x: hidden; @@ -149,26 +135,26 @@ $new-sidebar-collapsed-width: 50px;        display: flex;        align-items: center;        padding: 12px 16px; -      color: $inactive-color; +      color: $gl-text-color-secondary;      }      svg { -      fill: $inactive-color; +      fill: $gl-text-color-secondary;      } -  } -  .nav-item-name { -    flex: 1; -  } +    .nav-item-name { +      flex: 1; +    } -  li.active { -    > a { -      font-weight: $gl-font-weight-bold; +    &.active { +      > a { +        font-weight: $gl-font-weight-bold; +      }      }    }    @media (max-width: $screen-xs-max) { -    left: (-$new-sidebar-width); +    left: (-$contextual-sidebar-width);    }    .nav-icon-container { @@ -210,8 +196,8 @@ $new-sidebar-collapsed-width: 50px;        &:hover,        &:focus { -        background: $active-hover-background; -        color: $active-hover-color; +        background: $link-active-background; +        color: $gl-text-color;        }      } @@ -220,7 +206,7 @@ $new-sidebar-collapsed-width: 50px;          &,          &:hover,          &:focus { -          background: $active-background; +          background: $link-active-background;          }        }      } @@ -308,11 +294,11 @@ $new-sidebar-collapsed-width: 50px;      .badge {        background-color: $inactive-badge-background; -      color: $inactive-color; +      color: $gl-text-color-secondary;      }      &.active { -      background: $active-background; +      background: $link-active-background;        > a {          margin-left: 4px; @@ -330,7 +316,7 @@ $new-sidebar-collapsed-width: 50px;      &.active > a:hover,      &.is-over > a { -      background-color: $white-light; +      background-color: $link-hover-background;      }    }  } @@ -340,11 +326,11 @@ $new-sidebar-collapsed-width: 50px;  .toggle-sidebar-button,  .close-nav-button { -  width: $new-sidebar-width - 2px; +  width: $contextual-sidebar-width - 2px;    position: fixed;    bottom: 0;    padding: 16px; -  background-color: $gray-normal; +  background-color: $gray-light;    border: 0;    border-top: 2px solid $border-color;    color: $gl-text-color-secondary; @@ -407,7 +393,7 @@ $new-sidebar-collapsed-width: 50px;    }    .toggle-sidebar-button { -    width: $new-sidebar-collapsed-width - 2px; +    width: $contextual-sidebar-collapsed-width - 2px;      padding: 16px;      .collapse-text, @@ -466,7 +452,7 @@ $new-sidebar-collapsed-width: 50px;    @media (max-width: $screen-xs-max) {      + .breadcrumbs-links { -      padding-left: 17px; +      padding-left: $gl-padding;        border-left: 1px solid $gl-text-color-quaternary;      }    } @@ -498,10 +484,7 @@ $new-sidebar-collapsed-width: 50px;    height: calc(100vh - #{$header-height});    @media (min-width: $screen-sm-min) { -    height: 475px; // Needed for PhantomJS -    // scss-lint:disable DuplicateProperty      height: calc(100vh - 180px); -    // scss-lint:enable DuplicateProperty    }  } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 5b950ae0ba0..579bd48fac6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -37,6 +37,7 @@    .dropdown-menu-nav {      @include set-visible;      display: block; +    min-height: 40px;      @media (max-width: $screen-xs-max) {        width: 100%; @@ -727,11 +728,11 @@  .pika-single.animate-picker.is-bound {    @include set-visible; -} -.pika-single.animate-picker.is-bound.is-hidden { -  @include set-invisible; -  overflow: hidden; +  &.is-hidden { +    @include set-invisible; +    overflow: hidden; +  }  }  @mixin dropdown-item-hover { @@ -749,7 +750,7 @@        margin-bottom: $dropdown-vertical-offset;      } -    li:not(.dropdown-bold-header) { +    li {        display: block;        padding: 0 1px; @@ -776,12 +777,15 @@        a,        button,        .menu-item { +        margin-bottom: 0;          border-radius: 0;          box-shadow: none;          padding: 8px 16px;          text-align: left;          white-space: normal;          width: 100%; +        font-weight: $gl-font-weight-normal; +        line-height: normal;          &.dropdown-menu-user-link {            white-space: nowrap; @@ -838,6 +842,7 @@          a {            padding: 8px 40px; +          &.is-indeterminate::before,            &.is-active::before {              left: 16px;            } @@ -934,9 +939,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {        border-right: 0;      }    } -} -.projects-dropdown-container {    .projects-list-frequent-container,    .projects-list-search-container, {      padding: 8px 0; @@ -947,11 +950,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {    .projects-list-frequent-container li.section-empty,    .projects-list-search-container li.section-empty {      padding: 0 15px; -  } - -  .section-header, -  .projects-list-frequent-container li.section-empty, -  .projects-list-search-container li.section-empty {      color: $gl-text-color-secondary;      font-size: $gl-font-size;    } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 588ec1ff3bc..1247e5e4876 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -10,6 +10,10 @@      border: 0;    } +  &.file-holder-bottom-radius { +    border-radius: 0 0 $border-radius-small $border-radius-small; +  } +    &.readme-holder {      margin: $gl-padding 0; @@ -138,7 +142,7 @@       */      &.blame {        table { -        border: none; +        border: 0;          margin: 0;        } @@ -146,37 +150,51 @@          border-bottom: 1px solid $blame-border;          &:last-child { -          border-bottom: none; +          border-bottom: 0;          }        }        td { -        border-top: none; -        border-bottom: none; +        border-top: 0; +        border-bottom: 0;          &:first-child { -          border-left: none; +          border-left: 0;          }          &:last-child { -          border-right: none; +          border-right: 0; +        } + +        &.blame-commit { +          padding: 5px 10px; +          min-width: 400px; +          max-width: 400px; +          background: $gray-light; +          border-left: 3px solid; + +          .commit-row-title { +            display: flex; +          } + +          .item-title { +            flex: 1; +            margin-right: 0.5em; +          }          } -      } -      td.blame-commit { -        padding: 5px 10px; -        min-width: 400px; -        max-width: 400px; -        background: $gray-light; -        border-left: 3px solid; +        &.line-numbers { +          float: none; +          border-left: 1px solid $blame-line-numbers-border; -        .commit-row-title { -          display: flex; +          i { +            float: none; +            margin-right: 0; +          }          } -        .item-title { -          flex: 1; -          margin-right: 0.5em; +        &.lines { +          padding: 0;          }        } @@ -191,20 +209,6 @@            border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%);          }        } - -      td.line-numbers { -        float: none; -        border-left: 1px solid $blame-line-numbers-border; - -        i { -          float: none; -          margin-right: 0; -        } -      } - -      td.lines { -        padding: 0; -      }      }      &.logs { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index b2847c348eb..74b6b31b07e 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -65,7 +65,7 @@      display: flex;      flex: 1;      -webkit-flex: 1; -    padding-left: 30px; +    padding-left: 12px;      position: relative;      margin-bottom: 0;    } @@ -221,10 +221,6 @@      box-shadow: 0 0 4px $search-input-focus-shadow-color;    } -  &.focus .fa-filter { -    color: $common-gray-dark; -  } -    gl-emoji {      display: inline-block;      font-family: inherit; @@ -251,13 +247,6 @@      }    } -  .fa-filter { -    position: absolute; -    top: 10px; -    left: 10px; -    color: $gray-darkest; -  } -    .fa-times {      right: 10px;      color: $gray-darkest; @@ -266,7 +255,7 @@    .clear-search {      width: 35px;      background-color: $white-light; -    border: none; +    border: 0;      outline: none;      z-index: 1; @@ -279,12 +268,6 @@  .filtered-search-box-input-container {    flex: 1;    position: relative; -  // Fix PhantomJS not supporting `flex: 1;` properly. -  // This is important because it can change the expected `e.target` when clicking things in tests. -  // See https://gitlab.com/gitlab-org/gitlab-ce/blob/b54acba8b732688c59fe2f38510c469dc86ee499/spec/features/issues/filtered_search/visual_tokens_spec.rb#L61 -  // - With `width: 100%`: `e.target` = `.tokens-container`, https://i.imgur.com/jGq7wbx.png -  // - Without `width: 100%`: `e.target` = `.filtered-search`, https://i.imgur.com/cNI2CyT.png -  width: 100%;    min-width: 0;  } @@ -435,7 +418,7 @@  .droplab-dropdown .dropdown-menu .filter-dropdown-item {    .btn { -    border: none; +    border: 0;      width: 100%;      text-align: left;      padding: 8px 16px; @@ -480,10 +463,10 @@        word-break: break-all;      }    } -} -.filter-dropdown-item.droplab-item-active .btn { -  @extend %filter-dropdown-item-btn-hover; +  &.droplab-item-active .btn { +    @extend %filter-dropdown-item-btn-hover; +  }  }  .filter-dropdown-loading { diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 52b87de7a3d..dc591c06c88 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -216,12 +216,9 @@ body {              color: $theme-gray-900;            } -          &.active > a { +          &.active > a, +          &.active > a:hover {              color: $white-light; - -            &:hover { -              color: $white-light; -            }            }          }        } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 22945e935ef..1cdfa904374 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -10,7 +10,7 @@      z-index: 1000;      margin-bottom: 0;      min-height: $header-height; -    border: none; +    border: 0;      border-bottom: 1px solid $border-color;      position: fixed;      top: 0; @@ -161,14 +161,15 @@      }    } -  .dropdown-bold-header { +  li.dropdown-bold-header {      color: $gl-text-color-secondary;      font-size: 12px; +    padding: 0 16px;    }    .navbar-collapse {      flex: 0 0 auto; -    border-top: none; +    border-top: 0;      padding: 0;      @media (max-width: $screen-xs-max) { @@ -238,10 +239,8 @@              fill: currentColor;            } -          &.header-user-dropdown-toggle { -            .header-user-avatar { -              border-color: $white-light; -            } +          &.header-user-dropdown-toggle .header-user-avatar { +            border-color: $white-light;            }          }        } diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index c63114f85b4..813a1711ea2 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -1,5 +1,5 @@  .file-content.code { -  border: none; +  border: 0;    box-shadow: none;    margin: 0;    padding: 0; @@ -7,7 +7,7 @@    pre {      padding: 10px 0; -    border: none; +    border: 0;      border-radius: 0;      font-family: $monospace_font;      font-size: $code_font_size; diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 69d19ea2962..cb324ccc440 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -30,10 +30,10 @@ body {  .container {    padding-top: 0;    z-index: 5; -} -.container .content { -  margin: 0; +  .content { +    margin: 0; +  }  }  .navless-container { @@ -82,26 +82,26 @@ body {      transition: background-color 0.15s, border-color 0.15s;      background-color: $orange-500;      border-color: $orange-500; -  } -  .alert-warning + .alert-warning { -    background-color: $orange-600; -    border-color: $orange-600; -  } +    &:only-of-type { +      background-color: $orange-500; +      border-color: $orange-500; +    } -  .alert-warning + .alert-warning + .alert-warning { -    background-color: $orange-700; -    border-color: $orange-700; -  } +    + .alert-warning { +      background-color: $orange-600; +      border-color: $orange-600; -  .alert-warning + .alert-warning + .alert-warning + .alert-warning { -    background-color: $orange-800; -    border-color: $orange-800; -  } +      + .alert-warning { +        background-color: $orange-700; +        border-color: $orange-700; -  .alert-warning:only-of-type { -    background-color: $orange-500; -    border-color: $orange-500; +        + .alert-warning { +          background-color: $orange-800; +          border-color: $orange-800; +        } +      } +    }    }  } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index badc7b0eba3..ad3bb0e35d1 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -42,7 +42,7 @@      }      &:last-child { -      border-bottom: none; +      border-bottom: 0;        &.bottom {          background: $gray-light; @@ -92,7 +92,7 @@ ul.unstyled-list {  }  ul.unstyled-list > li { -  border-bottom: none; +  border-bottom: 0;  }  // Generic content list @@ -178,7 +178,7 @@ ul.content-list {      // When dragging a list item      &.ui-sortable-helper { -      border-bottom: none; +      border-bottom: 0;      }      &.list-placeholder { @@ -281,7 +281,58 @@ ul.indent-list {  // Specific styles for tree list +@keyframes spin-avatar { +  from { transform: rotate(0deg); } +  to { transform: rotate(360deg); } +} + +.groups-list-tree-container { +  .has-no-search-results { +    text-align: center; +    padding: $gl-padding; +    font-style: italic; +    color: $well-light-text-color; +  } + +  > .group-list-tree > .group-row.has-children:first-child { +    border-top: 0; +  } +} +  .group-list-tree { +  .avatar-container.content-loading { +    position: relative; + +    > a, +    > a .avatar { +      height: 100%; +      border-radius: 50%; +    } + +    > a { +      padding: 2px; + +      .avatar { +        border: 2px solid $white-normal; + +        &.identicon { +          line-height: 30px; +        } +      } +    } + +    &::after { +      content: ""; +      position: absolute; +      height: 100%; +      width: 100%; +      background-color: transparent; +      border: 2px outset $kdb-border; +      border-radius: 50%; +      animation: spin-avatar 3s infinite linear; +    } +  } +    .folder-toggle-wrap {      float: left;      line-height: $list-text-height; @@ -293,7 +344,7 @@ ul.indent-list {    }    .folder-caret, -  .folder-icon { +  .item-type-icon {      display: inline-block;    } @@ -301,11 +352,11 @@ ul.indent-list {      width: 15px;    } -  .folder-icon { +  .item-type-icon {      width: 20px;    } -  > .group-row:not(.has-subgroups) { +  > .group-row:not(.has-children) {      .folder-caret .fa {        opacity: 0;      } @@ -351,12 +402,23 @@ ul.indent-list {          top: 30px;          bottom: 0;        } + +      &.being-removed { +        opacity: 0.5; +      }      }    }    .group-row {      padding: 0; -    border: none; + +    &.has-children { +      border-top: 0; +    } + +    &:first-child { +      border-top: 1px solid $white-normal; +    }      &:last-of-type {        .group-row-contents:not(:hover) { @@ -379,6 +441,25 @@ ul.indent-list {      .avatar-container > a {        width: 100%;      } + +    &.has-more-items { +      display: block; +      padding: 20px 10px; +    } +  } +} + +ul.group-list-tree { +  li.group-row { +    &.has-description { +      .title { +        line-height: inherit; +      } +    } + +    .title { +      line-height: $list-text-height; +    }    }  } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index e3920b5d3d9..cd6f94fb354 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -138,15 +138,23 @@  .toolbar-btn {    float: left; -  padding: 0 5px; -  color: $gl-text-color-secondary; +  padding: 0 7px;    background: transparent;    border: 0;    outline: 0; +  svg { +    width: 14px; +    height: 14px; +    margin-top: 3px; +    fill: $gl-text-color-secondary; +  } +    &:hover,    &:focus { -    color: $gl-link-color; +    svg { +      fill: $gl-link-color; +    }    }  } @@ -173,21 +181,8 @@    ul > li {      white-space: nowrap;    } -} -@media(max-width: $screen-xs-max) { -  .atwho-view-ul { -    width: 350px; -  } - -  .atwho-view ul li { -    overflow: hidden; -    text-overflow: ellipsis; -  } -} - -// TODO: fallback to global style -.atwho-view { +  // TODO: fallback to global style    .atwho-view-ul {      padding: 8px 1px; @@ -220,3 +215,14 @@      }    }  } + +@media(max-width: $screen-xs-max) { +  .atwho-view-ul { +    width: 350px; +  } + +  .atwho-view ul li { +    overflow: hidden; +    text-overflow: ellipsis; +  } +} diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 2fee2164190..16d5edde61e 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -36,7 +36,7 @@      margin: 0;      &:last-child { -      border-bottom: none; +      border-bottom: 0;      }      &.active { diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 1cebd02df48..5c9838c1029 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -7,6 +7,7 @@  }  .modal-body { +  background-color: $modal-body-bg;    position: relative;    padding: #{3 * $grid-size} #{2 * $grid-size}; @@ -42,3 +43,8 @@ body.modal-open {      width: 98%;    }  } + +.modal.popup-dialog { +  display: block; +} + diff --git a/app/assets/stylesheets/framework/responsive-tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss index 8e653c443cf..7829d722560 100644 --- a/app/assets/stylesheets/framework/responsive-tables.scss +++ b/app/assets/stylesheets/framework/responsive_tables.scss @@ -3,57 +3,77 @@    max-width: #{$max + '%'};  } +.gl-responsive-table-row-layout { +  width: 100%; + +  @media (min-width: $screen-md-min) { +    display: flex; +    align-items: center; + +    & > &:not(:first-child) { +      margin-top: $gl-padding; +    } +  } +} +  .gl-responsive-table-row { +  @extend .gl-responsive-table-row-layout;    margin-top: 10px;    border: 1px solid $border-color;    @media (min-width: $screen-md-min) { -    padding: 15px 0;      margin: 0; -    display: flex; -    align-items: center; -    border: none; -    border-bottom: 1px solid $white-normal; +    padding: $gl-padding 0; +    border: 0; + +    &:not(:last-child) { +      border-bottom: 1px solid $white-normal; +    }    } +} -  .table-section { -    white-space: nowrap; +.gl-responsive-table-row-col-span { +  flex-wrap: wrap; +} + +.table-section { +  white-space: nowrap; -    $section-widths: 10 15 20 25 30 40; -    @each $width in $section-widths { -      &.section-#{$width} { -        flex: 0 0 #{$width + '%'}; +  $section-widths: 10 15 20 25 30 40 100; +  @each $width in $section-widths { +    &.section-#{$width} { +      flex: 0 0 #{$width + '%'}; -        @media (min-width: $screen-md-min) { -          max-width: #{$width + '%'}; -        } +      @media (min-width: $screen-md-min) { +        max-width: #{$width + '%'};        }      } +  } -    &:not(.table-button-footer) { -      @media (max-width: $screen-sm-max) { -        display: flex; -        align-self: stretch; -        padding: 10px; -        align-items: center; -        min-height: 62px; +  @media (max-width: $screen-sm-max) { +    display: flex; +    align-self: stretch; +    padding: 10px; +    align-items: center; +    min-height: 62px; -        &:not(:first-of-type) { -          border-top: 1px solid $white-normal; -        } -      } +    &:not(:first-child) { +      border-top: 1px solid $white-normal;      } +  } -    &.section-wrap { -      white-space: normal; +  &.section-wrap { +    white-space: normal; -      @media (max-width: $screen-sm-max) { -        flex-wrap: wrap; -      } +    @media (max-width: $screen-sm-max) { +      flex-wrap: wrap;      }    } -} +  &.section-align-top { +    align-self: flex-start; +  } +}  .table-button-footer {    @media (min-width: $screen-md-min) { @@ -61,12 +81,13 @@    }    @media (max-width: $screen-sm-max) { -    background-color: $gray-normal; +    display: block;      align-self: stretch; +    min-height: 0; +    background-color: $gray-normal;      border-top: 1px solid $border-color;      .table-action-buttons { -      padding: 10px 5px;        display: flex;        .btn { @@ -77,7 +98,14 @@        > .external-url,        > .btn {          flex: 1 1 28px; -        margin: 0 5px; + +        &:not(:first-child) { +          margin-left: 5px; +        } + +        &:not(:last-child) { +          margin-right: 5px; +        }        }        .dropdown-new { diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss index 5c96b3b78e7..8498b37abe4 100644 --- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss +++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss @@ -6,6 +6,7 @@    margin: 0;    list-style: none;    height: auto; +  border-bottom: 1px solid $border-color;    li {      display: flex; @@ -24,6 +25,7 @@        &:focus {          text-decoration: none;          color: $black; +        border-bottom: 2px solid $gray-darkest;          .badge {            color: $black; @@ -61,7 +63,7 @@    .nav-links {      margin-bottom: 0; -    border-bottom: none; +    border-bottom: 0;      float: left;      &.wide { @@ -333,7 +335,7 @@      border-bottom: 1px solid $border-color;      .nav-links { -      border-bottom: none; +      border-bottom: 0;      }    }  } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 50f1445bc2e..bb70b270299 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -17,7 +17,7 @@      .select2-arrow {        background-image: none;        background-color: transparent; -      border: none; +      border: 0;        padding-top: 12px;        padding-right: 20px;        font-size: 10px; @@ -61,6 +61,11 @@    border: 1px solid $dropdown-border-color;    min-width: 175px;    color: $gl-text-color; +  z-index: 999; +} + +.select2-drop-mask { +  z-index: 998;  }  .select2-drop.select2-drop-above.select2-drop-active { @@ -68,11 +73,6 @@    margin-top: -6px;  } -.select2-results li.select2-result-with-children > .select2-result-label { -  font-weight: $gl-font-weight-bold; -  color: $gl-text-color; -} -  .select2-container-active {    .select2-choice,    .select2-choices { @@ -139,28 +139,28 @@    .select2-drop-auto-width & {      padding: 15px 15px 5px;    } -} -.select2-search input { -  padding: 2px 25px 2px 5px; -  background: $white-light image-url('select2.png'); -  background-repeat: no-repeat; -  background-position: right 0 bottom 6px; -  border: 1px solid $input-border; -  border-radius: $border-radius-default; -  transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; - -  &:focus { -    border-color: $input-border-focus; -  } -} +  input { +    padding: 2px 25px 2px 5px; +    background: $white-light image-url('select2.png'); +    background-repeat: no-repeat; +    background-position: right 0 bottom 6px; +    border: 1px solid $input-border; +    border-radius: $border-radius-default; +    transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; -.select2-search input.select2-active { -  background-color: $white-light; -  background-image: image-url('select2-spinner.gif') !important; -  background-repeat: no-repeat; -  background-position: right 5px center !important; -  background-size: 16px 16px !important; +    &:focus { +      border-color: $input-border-focus; +    } + +    &.select2-active { +      background-color: $white-light; +      background-image: image-url('select2-spinner.gif') !important; +      background-repeat: no-repeat; +      background-position: right 5px center !important; +      background-size: 16px 16px !important; +    } +  }  }  .select2-results { @@ -192,6 +192,11 @@    .select2-result {      padding: 0 1px;    } + +  li.select2-result-with-children > .select2-result-label { +    font-weight: $gl-font-weight-bold; +    color: $gl-text-color; +  }  }  .ajax-users-select { diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index ef58382ba41..1a19b7320a0 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -9,7 +9,7 @@      &.container-blank {        background: none;        padding: 0; -      border: none; +      border: 0;      }    }  } @@ -111,7 +111,7 @@      }      .block:last-of-type { -      border: none; +      border: 0;      }    } diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 4dd31bf28cd..5bde96caf42 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -33,7 +33,7 @@ table {        th {          background-color: $gray-light;          font-weight: $gl-font-weight-normal; -        border-bottom: none; +        border-bottom: 0;          &.wide {            width: 55%; diff --git a/app/assets/stylesheets/framework/tabs.scss b/app/assets/stylesheets/framework/tabs.scss new file mode 100644 index 00000000000..c8ba14b7066 --- /dev/null +++ b/app/assets/stylesheets/framework/tabs.scss @@ -0,0 +1,35 @@ +.gitlab-tabs { +  background: $gray-light; +  border: 1px solid $border-color; + +  li { +    width: 50%; + +    &:not(:last-child) { +      border-right: 1px solid $border-color; +    } + +    &.active { +      background: $white-light; +    } + +    a { +      width: 100%; +      text-align: center; +    } +  } +} + +.gitlab-tab-content { +  border: 1px solid $border-color; +  border-top: 0; +  margin-bottom: $gl-padding; + +  .tab-pane { +    padding: $gl-padding; + +    &.no-padding { +      padding: 0; +    } +  } +} diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index f718ec4bcad..373f35e71d8 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -21,7 +21,7 @@    }    &.text-file .diff-file { -    border-bottom: none; +    border-bottom: 0;    }  } @@ -66,5 +66,5 @@  .discussion .timeline-entry {    margin: 0; -  border-right: none; +  border-right: 0;  } diff --git a/app/assets/stylesheets/framework/tooltips.scss b/app/assets/stylesheets/framework/tooltips.scss index 93baf73cb78..98f28987a82 100644 --- a/app/assets/stylesheets/framework/tooltips.scss +++ b/app/assets/stylesheets/framework/tooltips.scss @@ -3,5 +3,5 @@    border-radius: $border-radius-default;    line-height: 16px;    font-weight: $gl-font-weight-normal; -  padding: $gl-btn-padding; +  padding: 8px;  } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 3ea77eb7a43..a23131e0818 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -164,3 +164,36 @@ $pre-border-color: $border-color;  $table-bg-accent: $gray-light;  $zindex-popover: 900; + +//== Modals +// +//## + +//** Padding applied to the modal body +$modal-inner-padding: $gl-padding; + +//** Padding applied to the modal title +$modal-title-padding: $gl-padding; +//** Modal title line-height +// $modal-title-line-height:     $line-height-base + +//** Background color of modal content area +$modal-content-bg: $gray-light; +$modal-body-bg: $white-light; +//** Modal content border color +// $modal-content-border-color:                   rgba(0,0,0,.2) +//** Modal content border color **for IE8** +// $modal-content-fallback-border-color:          #999 + +//** Modal backdrop background color +// $modal-backdrop-bg:           #000 +//** Modal backdrop opacity +// $modal-backdrop-opacity:      .5 +//** Modal header border color +// $modal-header-border-color:   #e5e5e5 +//** Modal footer border color +// $modal-footer-border-color:   $modal-header-border-color + +// $modal-lg:                    900px +// $modal-md:                    600px +// $modal-sm:                    300px diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 3c0b4c82d19..0817cce114c 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -167,7 +167,7 @@      &.plain-readme {        background: none; -      border: none; +      border: 0;        padding: 0;        margin: 0;        font-size: 14px; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index bbbd16322eb..8ab48e4844f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -9,6 +9,8 @@ $sidebar-transition-duration: .15s;  $sidebar-breakpoint: 1024px;  $default-transition-duration: .15s;  $right-sidebar-transition-duration: .3s; +$contextual-sidebar-width: 220px; +$contextual-sidebar-collapsed-width: 50px;  /*   * Color schema @@ -233,6 +235,7 @@ $container-text-max-width: 540px;  $gl-avatar-size: 40px;  $error-exclamation-point: $red-500;  $border-radius-default: 4px; +$border-radius-small: 2px;  $settings-icon-size: 18px;  $provider-btn-not-active-color: $blue-500;  $link-underline-blue: $blue-500; @@ -358,6 +361,13 @@ $filtered-search-term-shadow-color: rgba(0, 0, 0, 0.09);  $dropdown-hover-color: $blue-400;  /* +* Contextual Sidebar +*/ +$link-active-background: rgba(0, 0, 0, .04); +$link-hover-background: rgba(0, 0, 0, .06); +$inactive-badge-background: rgba(0, 0, 0, .08); + +/*  * Buttons  */  $btn-active-gray: #ececec; @@ -403,7 +413,6 @@ $note-targe3-inside: #ffffd3;  $note-line2-border: #ddd;  $note-icon-gutter-width: 55px; -  /*  * Zen  */ @@ -699,14 +708,6 @@ $perf-bar-bucket-color: #ccc;  $perf-bar-bucket-box-shadow-from: rgba($white-light, .2);  $perf-bar-bucket-box-shadow-to: rgba($black, .25); - -/* -Project Templates Icons -*/ -$rails: #c00; -$node: #353535; -$java: #70ad51; -  /*  Issuable warning  */ diff --git a/app/assets/stylesheets/framework/vue_transitions.scss b/app/assets/stylesheets/framework/vue_transitions.scss new file mode 100644 index 00000000000..e07a177e153 --- /dev/null +++ b/app/assets/stylesheets/framework/vue_transitions.scss @@ -0,0 +1,9 @@ +.fade-enter-active, +.fade-leave-active { +  transition: opacity $sidebar-transition-duration $general-hover-transition-curve; +} + +.fade-enter, +.fade-leave-to { +  opacity: 0; +} diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index 5f9756bf58a..68824ff8418 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -52,6 +52,37 @@    .label.label-gray {      background-color: $well-expand-item;    } + +  .branches { +    display: inline; +  } + +  .branch-link { +    margin-bottom: 2px; +  } + +  .limit-box { +    cursor: pointer; +    display: inline-flex; +    align-items: center; +    background-color: $red-100; +    border-radius: $border-radius-default; +    text-align: center; + +    &:hover { +      background-color: $red-200; +    } + +    .limit-icon { +      margin: 0 8px; +    } + +    .limit-message { +      line-height: 16px; +      margin-right: 8px; +      font-size: 12px; +    } +  }  }  .light-well { diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 0c226ff7598..5a4d3ba0ee9 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -9,7 +9,7 @@      z-index: 1031;      textarea { -      border: none; +      border: 0;        box-shadow: none;        border-radius: 0;        color: $black; @@ -57,7 +57,15 @@    padding: 5px;    font-size: 36px; +  svg { +    fill: $gl-text-color; +  } +    &:hover {      color: $black; + +    svg { +      fill: $black; +    }    }  } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 65b140cd7f8..c3d8f0c61a2 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -217,13 +217,31 @@ $white-gc-bg: #eaf2f5;    .cp { color: $white-cp; font-weight: $gl-font-weight-bold; }    .c1 { color: $white-c1; font-style: italic; }    .cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; } -  .gd { color: $white-gd; background-color: $white-gd-bg; } -  .gd .x { color: $white-gd-x; background-color: $white-gd-x-bg; } + +  .gd { +    color: $white-gd; +    background-color: $white-gd-bg; + +    .x { +      color: $white-gd-x; +      background-color: $white-gd-x-bg; +    } +  } +    .ge { font-style: italic; }    .gr { color: $white-gr; }    .gh { color: $white-gh; } -  .gi { color: $white-gi; background-color: $white-gi-bg; } -  .gi .x { color: $white-gi-x; background-color: $white-gi-x-bg; } + +  .gi { +    color: $white-gi; +    background-color: $white-gi-bg; + +    .x { +      color: $white-gi-x; +      background-color: $white-gi-x-bg; +    } +  } +    .go { color: $white-go; }    .gp { color: $white-gp; }    .gs { font-weight: $gl-font-weight-bold; } diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index fbe538ad1d7..658ac26fca9 100644 --- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -158,13 +158,31 @@ span.highlight_word {  .cp { color: $highlighted-cp; font-weight: $gl-font-weight-bold; }  .c1 { color: $highlighted-c1; font-style: italic; }  .cs { color: $highlighted-cs; font-weight: $gl-font-weight-bold; font-style: italic; } -.gd { color: $highlighted-gd; background-color: $highlighted-gd-bg; } -.gd .x { color: $highlighted-gd; background-color: $highlighted-gd-x-bg; } + +.gd { +  color: $highlighted-gd; +  background-color: $highlighted-gd-bg; + +  .x { +    color: $highlighted-gd; +    background-color: $highlighted-gd-x-bg; +  } +} +  .ge { font-style: italic; }  .gr { color: $highlighted-gr; }  .gh { color: $highlighted-gh; } -.gi { color: $highlighted-gi; background-color: $highlighted-gi-bg; } -.gi .x { color: $highlighted-gi; background-color: $highlighted-gi-x-bg; } + +.gi { +  color: $highlighted-gi; +  background-color: $highlighted-gi-bg; + +  .x { +    color: $highlighted-gi; +    background-color: $highlighted-gi-x-bg; +  } +} +  .go { color: $highlighted-go; }  .gp { color: $highlighted-gp; }  .gs { font-weight: $gl-font-weight-bold; } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index ca61f7a30c3..3683afa07de 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -72,7 +72,7 @@  }  .boards-list { -  height: calc(100vh - 152px); +  height: calc(100vh - 105px);    width: 100%;    padding-top: 25px;    padding-bottom: 25px; @@ -81,11 +81,12 @@    overflow-x: scroll;    white-space: nowrap; -  @media (min-width: $screen-sm-min) { -    height: 475px; // Needed for PhantomJS -    // scss-lint:disable DuplicateProperty -    height: calc(100vh - 222px); -    // scss-lint:enable DuplicateProperty +  @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { +    height: calc(100vh - 90px); +  } + +  @media (min-width: $screen-md-min) { +    height: calc(100vh - 160px);      min-height: 475px;    }  } @@ -414,7 +415,7 @@    margin: 5px;  } -.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar { +.page-with-contextual-sidebar.page-with-sidebar .issue-boards-sidebar {    .issuable-sidebar-header {      position: relative;    } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 50ec5110bf1..27b10b536a2 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -48,7 +48,7 @@      overflow-x: auto;      font-size: 12px;      border-radius: 0; -    border: none; +    border: 0;      .bash {        display: block; @@ -68,18 +68,18 @@      &.affix {        top: $header-height; -    } -    // with sidebar -    &.affix.sidebar-expanded { -      right: 306px; -      left: 16px; -    } +      // with sidebar +      &.sidebar-expanded { +        right: 306px; +        left: 16px; +      } -    // without sidebar -    &.affix.sidebar-collapsed { -      right: 16px; -      left: 16px; +      // without sidebar +      &.sidebar-collapsed { +        right: 16px; +        left: 16px; +      }      }      &.affix-top { @@ -333,8 +333,10 @@      svg {        position: relative; -      top: 2px; +      top: 3px;        margin-right: 3px; +      width: 14px; +      height: 14px;      }    } @@ -348,9 +350,10 @@      svg {        position: relative; -      top: 2px; +      top: 3px;        margin-right: 3px; -      height: 13px; +      height: 14px; +      width: 14px;      }      a { @@ -369,7 +372,7 @@      .build-job {        position: relative; -      .fa-arrow-right { +      .icon-arrow-right {          position: absolute;          left: 15px;          top: 20px; @@ -379,7 +382,7 @@        &.active {          font-weight: $gl-font-weight-bold; -        .fa-arrow-right { +        .icon-arrow-right {            display: block;          }        } @@ -392,8 +395,7 @@          background-color: $row-hover;        } -      .fa-refresh { -        font-size: 13px; +      .icon-retry {          margin-left: 3px;        }      } diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss index bf6a48889bf..fbe1f3081a0 100644 --- a/app/assets/stylesheets/pages/ci_projects.scss +++ b/app/assets/stylesheets/pages/ci_projects.scss @@ -36,7 +36,7 @@      pre.commit-message {        background: none;        padding: 0; -      border: none; +      border: 0;        margin: 20px 0;        border-radius: 0;      } diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 5538e46a6c4..e5b9e1f2de6 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -2,8 +2,9 @@    .clipboard-addon {      background-color: $white-light;    } +} -  .alert-block { -    margin-bottom: 20px; -  } +.cluster-applications-table { +  // Wait for the Vue to kick-in and render the applications block +  min-height: 302px;  } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index ee3ca246374..b1850be8a5f 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,6 +1,6 @@  .commit-description {    background: none; -  border: none; +  border: 0;    padding: 0;    margin-top: 10px;    word-break: normal; @@ -247,7 +247,7 @@      word-break: normal;      pre { -      border: none; +      border: 0;        background: inherit;        padding: 0;        margin: 0; diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 2a92673d9fa..292e0ad394b 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -22,6 +22,11 @@          }        }      } + +    svg { +      width: 136px; +      height: 136px; +    }    }    .col-headers { @@ -75,7 +80,7 @@    .panel {      .content-block {        padding: 24px 0; -      border-bottom: none; +      border-bottom: 0;        position: relative;        @media (max-width: $screen-xs-max) { @@ -155,11 +160,6 @@      }    } -  .landing svg { -    width: 136px; -    height: 136px; -  } -    .fa-spinner {      font-size: 28px;      position: relative; @@ -222,11 +222,11 @@        }        &:first-child { -        border-top: none; +        border-top: 0;        }        &:last-child { -        border-bottom: none; +        border-bottom: 0;        }        .stage-nav-item-cell { @@ -290,7 +290,7 @@      border-bottom: 1px solid $gray-darker;      &:last-child { -      border-bottom: none; +      border-bottom: 0;        margin-bottom: 0;      } diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 3d9eff35583..538e50ee306 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -3,6 +3,7 @@    border-bottom: 1px solid $border-color;    color: $gl-text-color;    line-height: 34px; +  display: flex;    a {      color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index ffb5fc94475..bce94e09367 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -47,7 +47,7 @@      table {        width: 100%;        font-family: $monospace_font; -      border: none; +      border: 0;        border-collapse: separate;        margin: 0;        padding: 0; @@ -105,7 +105,7 @@      .new_line {        @include user-select(none);        margin: 0; -      border: none; +      border: 0;        padding: 0 5px;        border-right: 1px solid;        text-align: right; @@ -133,7 +133,7 @@        display: block;        margin: 0;        padding: 0 1.5em; -      border: none; +      border: 0;        position: relative;        &.parallel { @@ -359,7 +359,7 @@        cursor: pointer;        &:first-child { -        border-left: none; +        border-left: 0;        }        &:hover { @@ -380,15 +380,15 @@        }      }    } + +  .line_content { +    white-space: pre-wrap; +  }  }  .file-content .diff-file {    margin: 0; -  border: none; -} - -.diff-file .line_content { -  white-space: pre-wrap; +  border: 0;  }  .diff-wrap-lines .line_content { @@ -400,7 +400,7 @@  }  .files-changed { -  border-bottom: none; +  border-bottom: 0;  }  .diff-stats-summary-toggler { @@ -707,11 +707,11 @@  .frame.click-to-comment {    position: relative; -  cursor: url(icon_image_comment.svg) +  cursor: image-url('icon_image_comment.svg')      $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;    // Retina cursor -  cursor: -webkit-image-set(url(icon_image_comment.svg) 1x, url(icon_image_comment@2x.svg) 2x) +  cursor: -webkit-image-set(image-url('icon_image_comment.svg') 1x, image-url('icon_image_comment@2x.svg') 2x)      $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;    .comment-indicator { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index d3cd4d507de..c586dab4cf2 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -3,13 +3,13 @@      border-top: 1px solid $border-color;      border-right: 1px solid $border-color;      border-left: 1px solid $border-color; -    border-bottom: none; -    border-radius: 2px; +    border-bottom: 0; +    border-radius: $border-radius-small $border-radius-small 0 0;      background: $gray-normal;    }    #editor { -    border: none; +    border: 0;      border-radius: 0;      height: 500px;      margin: 0; @@ -171,7 +171,7 @@      width: 100%;      margin: 5px 0;      padding: 0; -    border-left: none; +    border-left: 0;    }  } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 3b5e411e2c5..26c5f093c6b 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -117,7 +117,7 @@      }      .no-btn { -      border: none; +      border: 0;        background: none;        outline: none;        width: 100%; @@ -133,12 +133,11 @@  }  .folder-row { -  padding: 15px 0; -  border-bottom: 1px solid $white-normal; +  border-left: 0; +  border-right: 0; -  @media (max-width: $screen-sm-max) { -    border-top: 1px solid $white-normal; -    margin-top: 10px; +  @media (min-width: $screen-sm-max) { +    border-top: 0;    }  } @@ -256,29 +255,6 @@    width: 100%;    padding: 0;    padding-bottom: 100%; -} - -.prometheus-svg-container > svg { -  position: absolute; -  height: 100%; -  width: 100%; -  left: 0; -  top: 0; - -  text { -    fill: $gl-text-color; -    stroke-width: 0; -  } - -  .text-metric-bold { -    font-weight: $gl-font-weight-bold; -  } - -  .label-axis-text { -    fill: $black; -    font-weight: $gl-font-weight-normal; -    font-size: 10px; -  }    .text-metric-usage,    .legend-metric-title { @@ -287,42 +263,65 @@      font-size: 12px;    } -  .legend-axis-text { -    fill: $black; -  } +  > svg { +    position: absolute; +    height: 100%; +    width: 100%; +    left: 0; +    top: 0; -  .tick { -    > line { -      stroke: $gray-darker; +    text { +      fill: $gl-text-color; +      stroke-width: 0;      } -    > text { -      font-size: 12px; +    .text-metric-bold { +      font-weight: $gl-font-weight-bold;      } -  } -  .text-metric-title { -    font-size: 12px; -  } +    .label-axis-text { +      fill: $black; +      font-weight: $gl-font-weight-normal; +      font-size: 10px; +    } -  .y-label-text, -  .x-label-text { -    fill: $gray-darkest; -  } +    .legend-axis-text { +      fill: $black; +    } -  .axis-tick { -    stroke: $gray-darker; -  } +    .tick { +      > line { +        stroke: $gray-darker; +      } -  @media (max-width: $screen-sm-max) { -    .label-axis-text, -    .text-metric-usage, -    .legend-axis-text { -      font-size: 8px; +      > text { +        font-size: 12px; +      } +    } + +    .text-metric-title { +      font-size: 12px; +    } + +    .y-label-text, +    .x-label-text { +      fill: $gray-darkest; +    } + +    .axis-tick { +      stroke: $gray-darker;      } -    .tick > text { -      font-size: 8px; +    @media (max-width: $screen-sm-max) { +      .label-axis-text, +      .text-metric-usage, +      .legend-axis-text { +        font-size: 8px; +      } + +      .tick > text { +        font-size: 8px; +      }      }    }  } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 1723d716805..eea8b7dd193 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -85,7 +85,7 @@        }        pre { -        border: none; +        border: 0;          background: $gray-light;          border-radius: 0;          color: $events-pre-color; @@ -128,14 +128,14 @@      }    } -  &:last-child { border: none; } +  &:last-child { border: 0; }    .event_commits {      li {        &.commit {          background: transparent;          padding: 0; -        border: none; +        border: 0;          .commit-row-title {            font-size: $gl-font-size; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 6f6c6839975..9b7dda9b648 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -26,14 +26,117 @@    }  } -.groups-header { -  @media (min-width: $screen-sm-min) { -    .nav-links { -      width: 35%; +.group-nav-container .nav-controls { +  display: flex; +  align-items: flex-start; +  padding: $gl-padding-top 0; +  border-bottom: 1px solid $border-color; + +  .group-filter-form { +    flex: 1; +  } + +  .dropdown-menu-align-right { +    margin-top: 0; +  } + +  .new-project-subgroup { +    .dropdown-primary { +      min-width: 115px; +    } + +    .dropdown-toggle { +      .dropdown-btn-icon { +        pointer-events: none; +        color: inherit; +        margin-left: 0; +      }      } -    .nav-controls { -      width: 65%; +    .dropdown-menu { +      min-width: 280px; +      margin-top: 2px; +    } + +    li:not(.divider) { +      padding: 0; + +      &.droplab-item-selected { +        .icon-container { +          .list-item-checkmark { +            visibility: visible; +          } +        } +      } + +      .menu-item { +        padding: 8px 4px; + +        &:hover { +          background-color: $gray-darker; +          color: $theme-gray-900; +        } +      } + +      .icon-container { +        float: left; +        padding-left: 6px; + +        .list-item-checkmark { +          visibility: hidden; +        } +      } + +      .description { +        font-size: 14px; + +        strong { +          display: block; +          font-weight: $gl-font-weight-bold; +        } +      } +    } +  } + +  @media (max-width: $screen-sm-max) { +    &, +    .dropdown, +    .dropdown .dropdown-toggle, +    .btn-new { +      display: block; +    } + +    .group-filter-form, +    .dropdown { +      margin-bottom: 10px; +      margin-right: 0; +    } + +    .group-filter-form, +    .dropdown .dropdown-toggle, +    .btn-new { +      width: 100%; +    } + +    .dropdown .dropdown-toggle .fa-chevron-down { +      position: absolute; +      top: 11px; +      right: 8px; +    } + +    .new-project-subgroup { +      display: flex; +      align-items: flex-start; + +      .dropdown-primary { +        flex: 1; +      } + +      .dropdown-menu { +        width: 100%; +        max-width: inherit; +        min-width: inherit; +      }      }    }  } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index dae3ec7ac42..760c7c80aff 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -72,10 +72,20 @@      }    } +  .title-container { +    display: flex; +  } +    .title {      padding: 0;      margin-bottom: 16px; -    border-bottom: none; +    border-bottom: 0; +  } + +  .btn-edit { +    margin-left: auto; +    // Set height to match title height +    height: 2em;    }    // Border around images in issue and MR descriptions. @@ -117,7 +127,16 @@  }  .right-sidebar { -  a, +  position: absolute; +  top: $header-height; +  bottom: 0; +  right: 0; +  transition: width $right-sidebar-transition-duration; +  background: $gray-light; +  z-index: 200; +  overflow: hidden; + +  a:not(.btn-retry),    .btn-link {      color: inherit;    } @@ -145,7 +164,7 @@      }      &:last-child { -      border: none; +      border: 0;      }      span { @@ -218,17 +237,6 @@    .btn-clipboard:hover {      color: $gl-text-color;    } -} - -.right-sidebar { -  position: absolute; -  top: $header-height; -  bottom: 0; -  right: 0; -  transition: width $right-sidebar-transition-duration; -  background: $gray-light; -  z-index: 200; -  overflow: hidden;    .issuable-sidebar {      width: calc(100% + 100px); @@ -330,7 +338,7 @@      .block {        width: $gutter_collapsed_width - 2px;        padding: 15px 0 0; -      border-bottom: none; +      border-bottom: 0;        overflow: hidden;      } @@ -391,7 +399,7 @@        }        .btn-clipboard { -        border: none; +        border: 0;          color: $issuable-sidebar-color;          &:hover { @@ -459,7 +467,7 @@      }    } -  a { +  a:not(.btn-retry) {      &:hover {        color: $md-link-color;        text-decoration: none; @@ -532,7 +540,9 @@  }  .participants-list { -  margin: -5px; +  display: flex; +  flex-wrap: wrap; +  margin: -7px;  } @@ -543,7 +553,7 @@  .participants-author {    display: inline-block; -  padding: 5px; +  padding: 7px;    &:nth-of-type(7n) {      padding-right: 0; @@ -603,6 +613,8 @@    float: none;    display: inline-block;    margin-top: 0; +  height: auto; +  align-self: center;    @media (max-width: $screen-xs-max) {      position: absolute; @@ -616,6 +628,8 @@    padding-left: 45px;    padding-right: 45px;    line-height: 35px; +  display: flex; +  flex-grow: 1;    @media (min-width: $screen-sm-min) {      float: left; @@ -627,11 +641,12 @@  .issuable-actions {    @include new-style-dropdown; -  padding-top: 10px; +  align-self: center; +  flex-shrink: 0; +  flex: 0 0 auto;    @media (min-width: $screen-sm-min) {      float: right; -    padding-top: 0;    }  } @@ -645,8 +660,9 @@  .issuable-meta {    display: inline-block; -  line-height: 18px;    font-size: 14px; +  line-height: 24px; +  align-self: center;  }  .js-issuable-selector-wrap { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index e8ca5cedaee..8bb68ad2425 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -134,11 +134,24 @@ ul.related-merge-requests > li {  }  @media (max-width: $screen-xs-max) { -  .issue-btn-group { -    width: 100%; +  .detail-page-header, +  .issuable-header { +    display: block; + +    .issuable-meta { +      line-height: 18px; +    } +  } -    .btn { +  .issuable-actions { +    margin-top: 10px; + +    .issue-btn-group {        width: 100%; + +      .btn { +        width: 100%; +      }      }    }  } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index cf5f933a762..b7985c4dea5 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -109,13 +109,37 @@      border-top-right-radius: $border-radius-default;      border-top-left-radius: $border-radius-default; +    // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long). +    // These styles prevent this from breaking the layout, and only applied when providers are configured. +    &.custom-provider-tabs { +      flex-wrap: wrap; + +      li { +        min-width: 85px; +        flex-basis: auto; + +        // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen. +        // We are making somewhat of an assumption about the configuration here: that users do not have more than +        // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any +        // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border +        // above one of the bottom row elements. If you know a better way, please implement it! +        &:nth-child(n+5) { +          border-top: 1px solid $border-color; +        } +      } + +      a { +        font-size: 16px; +      } +    } +      li {        flex: 1;        text-align: center;        border-left: 1px solid $border-color;        &:first-of-type { -        border-left: none; +        border-left: 0;          border-top-left-radius: $border-radius-default;        } @@ -141,7 +165,7 @@          border-bottom: 1px solid $border-color;          a { -          border: none; +          border: 0;            border-bottom: 2px solid $link-underline-blue;            margin-right: 0;            color: $black; @@ -154,32 +178,6 @@      }    } -  // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long). -  // These styles prevent this from breaking the layout, and only applied when providers are configured. - -  .new-session-tabs.custom-provider-tabs { -    flex-wrap: wrap; - -    li { -      min-width: 85px; -      flex-basis: auto; - -      // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen. -      // We are making somewhat of an assumption about the configuration here: that users do not have more than -      // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any -      // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border -      // above one of the bottom row elements. If you know a better way, please implement it! -      &:nth-child(n+5) { -        border-top: 1px solid $border-color; -      } -    } - -    a { -      font-size: 16px; -    } -  } - -    .form-control {      &:active,      &:focus { @@ -231,35 +229,35 @@    margin: 0;    padding: 0;    height: 100%; -} -// Fixes footer container to bottom of viewport -.devise-layout-html body { -  // offset height of fixed header + 1 to avoid scroll -  height: calc(100% - 51px); -  margin: 0; -  padding: 0; +  // Fixes footer container to bottom of viewport +  body { +    // offset height of fixed header + 1 to avoid scroll +    height: calc(100% - 51px); +    margin: 0; +    padding: 0; -  .page-wrap { -    min-height: 100%; -    position: relative; -  } +    .page-wrap { +      min-height: 100%; +      position: relative; +    } -  .footer-container, -  hr.footer-fixed { -    position: absolute; -    bottom: 0; -    left: 0; -    right: 0; -    height: 40px; -    background: $white-light; -  } +    .footer-container, +    hr.footer-fixed { +      position: absolute; +      bottom: 0; +      left: 0; +      right: 0; +      height: 40px; +      background: $white-light; +    } -  .navless-container { -    padding: 65px 15px; // height of footer + bottom padding of email confirmation link +    .navless-container { +      padding: 65px 15px; // height of footer + bottom padding of email confirmation link -    @media (max-width: $screen-xs-max) { -      padding: 0 15px 65px; +      @media (max-width: $screen-xs-max) { +        padding: 0 15px 65px; +      }      }    }  } diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index b3bab082a35..18c48405ecd 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -3,41 +3,12 @@    border-bottom: 1px solid $border-color;  } -.project-member-tabs { -  background: $gray-light; -  border: 1px solid $border-color; - -  li { -    width: 50%; - -    &.active { -      background: $white-light; -    } - -    &:first-child { -      border-right: 1px solid $border-color; -    } - -    a { -      width: 100%; -      text-align: center; -    } -  } -} -  .users-project-form {    .btn-create {      margin-right: 10px;    }  } -.project-member-tab-content { -  padding: $gl-padding; -  border: 1px solid $border-color; -  border-top: 0; -  margin-bottom: $gl-padding; -} -  .member {    .list-item-name {      @media (min-width: $screen-sm-min) { @@ -78,9 +49,17 @@        width: auto;      }    } + +  &.existing-title { +    @media (min-width: $screen-sm-min) { +      float: left; +    } +  }  }  .member-form-control { +  @include new-style-dropdown; +    @media (max-width: $screen-xs-max) {      padding-bottom: 5px;      margin-left: 0; @@ -93,12 +72,6 @@    line-height: 43px;  } -.member.existing-title { -  @media (min-width: $screen-sm-min) { -    float: left; -  } -} -  .member-search-form {    @include new-style-dropdown; @@ -310,7 +283,3 @@      }    }  } - -.member-form-control { -  @include new-style-dropdown; -} diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index dbf3e2b763c..04bde64c752 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -262,7 +262,7 @@ $colors: (      .editor {        pre {          height: 350px; -        border: none; +        border: 0;          border-radius: 0;          margin-bottom: 0;        } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index d9fb3b44d29..5832cf4637f 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -150,14 +150,6 @@      display: block;    } -  .mr-widget-body { -    @include clearfix; - -    &.media > *:first-child { -      margin-right: 10px; -    } -  } -    .mr-widget-pipeline-graph {      padding: 0 4px; @@ -209,12 +201,17 @@      }    } -  .mr-widget-help { -    padding: 10px 16px 10px 48px; -    font-style: italic; -  } -    .mr-widget-body { +    @include clearfix; + +    &.media > *:first-child { +      margin-right: 10px; +    } + +    .approve-btn { +      margin-right: 5px; +    } +      h4 {        float: left;        font-weight: $gl-font-weight-bold; @@ -336,6 +333,11 @@      }    } +  .mr-widget-help { +    padding: 10px 16px 10px 48px; +    font-style: italic; +  } +    .ci-coverage {      float: right;    } @@ -350,12 +352,6 @@    }  } -.mr-state-widget .mr-widget-body { -  .approve-btn { -    margin-right: 5px; -  } -} -  .mr-widget-body-controls {    flex-wrap: wrap;  } @@ -469,16 +465,16 @@        padding-bottom: 0;      }    } -} -.mr-info-list.mr-memory-usage { -  p { -    float: left; -  } +  &.mr-memory-usage { +    p { +      float: left; +    } -  .memory-graph-container { -    float: left; -    margin-left: 5px; +    .memory-graph-container { +      float: left; +      margin-left: 5px; +    }    }  } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 32039936be7..ae8fa45a2d7 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -66,6 +66,15 @@        height: 6px;        margin: 0;      } + +    .sidebar-collapsed-icon { +      clear: both; +      padding: 15px 5px 5px; + +      .progress { +        margin: 5px 0; +      } +    }    }    .collapsed-milestone-date { @@ -93,17 +102,6 @@      margin-right: 0;    } -  .milestone-progress { -    .sidebar-collapsed-icon { -      clear: both; -      padding: 15px 5px 5px; - -      .progress { -        margin: 5px 0; -      } -    } -  } -    .right-sidebar-collapsed & {      .reference {        border-top: 1px solid $border-gray-normal; @@ -156,18 +154,16 @@    .status-box {      margin-top: 0; -  } - -  .milestone-buttons { -    margin-left: auto; -  } - -  .status-box {      order: 1;    }    .milestone-buttons { +    margin-left: auto;      order: 2; + +    .verbose { +      display: none; +    }    }    .header-text-content { @@ -175,10 +171,6 @@      width: 100%;    } -  .milestone-buttons .verbose { -    display: none; -  } -    @media (min-width: $screen-xs-min) {      .milestone-buttons .verbose {        display: inline; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 04b132415eb..14514b2f193 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -16,7 +16,7 @@  .discussion {    .new-note {      margin: 0; -    border: none; +    border: 0;    }  } @@ -106,7 +106,7 @@    background-color: $orange-100;    border-radius: $border-radius-default $border-radius-default 0 0;    border: 1px solid $border-gray-normal; -  border-bottom: none; +  border-bottom: 0;    padding: 3px 12px;    margin: auto;    align-items: center; @@ -114,21 +114,26 @@    .icon {      margin-right: $issuable-warning-icon-margin;    } -} -.disabled-comment .issuable-note-warning { -  border: none; -  border-radius: $label-border-radius; -  padding-top: $gl-vert-padding; -  padding-bottom: $gl-vert-padding; +  + .md-area { +    border-top-left-radius: 0; +    border-top-right-radius: 0; +  } -  .icon svg { -    position: relative; -    top: 2px; -    margin-right: $btn-xs-side-margin; -    width: $gl-font-size; -    height: $gl-font-size; -    fill: $orange-600; +  .disabled-comment { +    border: 0; +    border-radius: $label-border-radius; +    padding-top: $gl-vert-padding; +    padding-bottom: $gl-vert-padding; + +    .icon svg { +      position: relative; +      top: 2px; +      margin-right: $btn-xs-side-margin; +      width: $gl-font-size; +      height: $gl-font-size; +      fill: $orange-600; +    }    }  } @@ -155,11 +160,6 @@    }  } -.issuable-note-warning + .md-area { -  border-top-left-radius: 0; -  border-top-right-radius: 0; -} -  .discussion-form {    background-color: $white-light;  } @@ -249,13 +249,12 @@        width: 100%;        padding-right: 5px;      } -    }    .discussion-actions {      display: table; -    .new-issue-for-discussion path { +    .btn-default path {        fill: $gray-darkest;      } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 96b7db3b85d..9537eeeee97 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -269,7 +269,7 @@ ul.notes {            display: none;          } -        &.system-note-commit-list { +        &.system-note-commit-list:not(.hide-shade) {            max-height: 70px;            overflow: hidden;            display: block; @@ -291,16 +291,6 @@ ul.notes {              bottom: 0;              background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%);            } - -          &.hide-shade { -            max-height: 100%; -            overflow: auto; - -            &::after { -              display: none; -              background: transparent; -            } -          }          }        }      } @@ -322,57 +312,72 @@ ul.notes {    }  } -.diff-file .notes_holder { -  font-family: $regular_font; +.diff-file { +  .is-over { +    .add-diff-note { +      display: inline-block; +    } +  } -  td { -    border: 1px solid $white-normal; -    border-left: none; +  // Merge request notes in diffs +  // Diff is inline +  .notes_content .note-header .note-headline-light { +    display: inline-block; +    position: relative; +  } -    &.notes_line { -      vertical-align: middle; -      text-align: center; -      padding: 10px 0; -      background: $gray-light; -      color: $text-color; -    } +  .notes_holder { +    font-family: $regular_font; -    &.notes_line2 { -      text-align: center; -      padding: 10px 0; -      border-left: 1px solid $note-line2-border !important; -    } +    td { +      border: 1px solid $white-normal; +      border-left: 0; -    &.notes_content { -      background-color: $gray-light; -      border-width: 1px 0; -      padding: 0; -      vertical-align: top; -      white-space: normal; +      &.notes_line { +        vertical-align: middle; +        text-align: center; +        padding: 10px 0; +        background: $gray-light; +        color: $text-color; +      } -      &.parallel { -        border-width: 1px; +      &.notes_line2 { +        text-align: center; +        padding: 10px 0; +        border-left: 1px solid $note-line2-border !important;        } -      .discussion-notes { -        &:not(:first-child) { -          border-top: 1px solid $white-normal; -          margin-top: 20px; +      &.notes_content { +        background-color: $gray-light; +        border-width: 1px 0; +        padding: 0; +        vertical-align: top; +        white-space: normal; + +        &.parallel { +          border-width: 1px;          } -        &:not(:last-child) { -          border-bottom: 1px solid $white-normal; -          margin-bottom: 20px; +        .discussion-notes { +          &:not(:first-child) { +            border-top: 1px solid $white-normal; +            margin-top: 20px; +          } + +          &:not(:last-child) { +            border-bottom: 1px solid $white-normal; +            margin-bottom: 20px; +          }          } -      } -      .notes { -        background-color: $white-light; -      } +        .notes { +          background-color: $white-light; +        } -      a code { -        top: 0; -        margin-right: 0; +        a code { +          top: 0; +          margin-right: 0; +        }        }      }    } @@ -466,6 +471,15 @@ ul.notes {    float: right;    margin-left: 10px;    color: $gray-darkest; + +  @include notes-media('max', $screen-md-max) { +    float: none; +    margin-left: 0; +  } + +  .btn-group > .discussion-next-btn { +    margin-left: -1px; +  }  }  .note-actions { @@ -475,8 +489,6 @@ ul.notes {    flex-shrink: 0;    display: inline-flex;    align-items: center; -  // For PhantomJS that does not support flex -  float: right;    margin-left: 10px;    color: $gray-darkest; @@ -487,7 +499,6 @@ ul.notes {  }  .more-actions { -  float: right; // phantomjs fallback    display: flex;    align-items: flex-end; @@ -508,13 +519,6 @@ ul.notes {    min-width: 180px;  } -.discussion-actions { -  @include notes-media('max', $screen-md-max) { -    float: none; -    margin-left: 0; -  } -} -  .note-actions-item {    margin-left: 12px;    display: flex; @@ -531,14 +535,13 @@ ul.notes {    padding: 0;    min-width: 16px;    color: $gray-darkest; +  fill: $gray-darkest;    .fa {      position: relative;      font-size: 16px;    } - -    svg {      height: 16px;      width: 16px; @@ -566,6 +569,7 @@ ul.notes {      .link-highlight {        color: $gl-link-color; +      fill: $gl-link-color;        svg {          fill: $gl-link-color; @@ -666,15 +670,7 @@ ul.notes {      .timeline-entry-inner {        padding-left: $gl-padding;        padding-right: $gl-padding; -      border-bottom: none; -    } -  } -} - -.diff-file { -  .is-over { -    .add-diff-note { -      display: inline-block; +      border-bottom: 0;      }    }  } @@ -687,7 +683,7 @@ ul.notes {    padding: 90px 0;    &.discussion-locked { -    border: none; +    border: 0;      background-color: $white-light;    } @@ -720,20 +716,20 @@ ul.notes {      svg path {        fill: $gray-darkest;      } -  } -  .btn.discussion-create-issue-btn { -    margin-left: -4px; -    border-radius: 0; -    border-right: 0; +    &.discussion-create-issue-btn { +      margin-left: -4px; +      border-radius: 0; +      border-right: 0; -    a { -      padding: 0; -      line-height: 0; +      a { +        padding: 0; +        line-height: 0; -      &:hover { -        text-decoration: none; -        border: 0; +        &:hover { +          text-decoration: none; +          border: 0; +        }        }      }    } @@ -767,7 +763,7 @@ ul.notes {    top: 0;    padding: 0;    background-color: transparent; -  border: none; +  border: 0;    outline: 0;    color: $gray-darkest;    transition: color $general-hover-transition-duration $general-hover-transition-curve; @@ -807,12 +803,3 @@ ul.notes {  .line-resolve-text {    vertical-align: middle;  } - -// Merge request notes in diffs -.diff-file { -  // Diff is inline -  .notes_content .note-header .note-headline-light { -    display: inline-block; -    position: relative; -  } -} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 8fc7a5eec9b..cb24274c612 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -31,7 +31,6 @@      }      .pipeline-actions { -      padding-right: 0;        min-width: 170px; //Guarantees buttons don't break in several lines.        .btn-default { @@ -176,6 +175,25 @@      }    } +  /** +   * Play button with icon in dropdowns +   */ +  .no-btn { +    border: 0; +    background: none; +    outline: none; +    width: 100%; +    text-align: left; + +    .icon-play { +      position: relative; +      top: 2px; +      margin-right: 5px; +      height: 13px; +      width: 12px; +    } +  } +    .duration,    .finished-at {      color: $gl-text-color-secondary; @@ -270,7 +288,7 @@  .pipeline-actions {    @include new-style-dropdown; -  border-bottom: none; +  border-bottom: 0;  }  .tab-pane { @@ -300,7 +318,7 @@    }    .build-log { -    border: none; +    border: 0;      line-height: initial;    }  } @@ -368,13 +386,13 @@          // Remove right connecting horizontal line from first build in last stage          &:first-child {            &::after { -            border: none; +            border: 0;            }          }          // Remove right curved connectors from all builds in last stage          &:not(:first-child) {            &::after { -            border: none; +            border: 0;            }          }          // Remove opposite curve @@ -391,7 +409,7 @@          // Remove left curved connectors from all builds in first stage          &:not(:first-child) {            &::before { -            border: none; +            border: 0;            }          }          // Remove opposite curve @@ -451,36 +469,46 @@        @extend .build-content:hover;      } -    // Action Icons in big pipeline-graph nodes -    .ci-action-icon-container .ci-action-icon-wrapper { -      height: 30px; -      width: 30px; -      background: $white-light; -      border: 1px solid $border-color; -      border-radius: 100%; -      display: block; - -      &:hover { -        background-color: $stage-hover-bg; -        border: 1px solid $dropdown-toggle-active-border-color; -      } - -      svg { -        fill: $gl-text-color-secondary; -        position: relative; -        left: -1px; -        top: -1px; -      } - -      &:hover svg { -        fill: $gl-text-color; -      } -    } -      .ci-action-icon-container {        position: absolute;        right: 5px;        top: 5px; + +      // Action Icons in big pipeline-graph nodes +      &.ci-action-icon-wrapper { +        height: 30px; +        width: 30px; +        background: $white-light; +        border: 1px solid $border-color; +        border-radius: 100%; +        display: block; + +        &:hover { +          background-color: $stage-hover-bg; +          border: 1px solid $dropdown-toggle-active-border-color; + +          svg { +            fill: $gl-text-color; +          } +        } + +        svg { +          fill: $gl-text-color-secondary; +          position: relative; +          left: 5px; +          top: 2px; +          width: 18px; +          height: 18px; +        } + +        &.play { +          svg { +            width: #{$ci-action-icon-size - 8}; +            height: #{$ci-action-icon-size - 8}; +            left: 8px; +          } +        } +      }      }      .ci-status-icon svg { @@ -490,7 +518,7 @@      .dropdown-menu-toggle {        background-color: transparent; -      border: none; +      border: 0;        padding: 0;        &:focus { @@ -721,17 +749,50 @@ button.mini-pipeline-graph-dropdown-toggle {        svg {          fill: $gl-text-color-secondary; -        width: $ci-action-icon-size; -        height: $ci-action-icon-size; -        left: -6px; +        width: #{$ci-action-icon-size - 6}; +        height: #{$ci-action-icon-size - 6}; +        left: -3px;          position: relative; -        top: -3px; +        top: -2px; + +        &.icon-action-stop, +        &.icon-action-cancel { +          width: 12px; +          height: 12px; +          top: 1px; +          left: -1px; +        } + +        &.icon-action-play { +          width: 11px; +          height: 11px; +          top: 1px; +          left: 1px; +        } + +        &.icon-action-retry { +          width: 16px; +          height: 16px; +          top: 0; +          left: -3px; +        }        }        &:hover svg,        &:focus svg {          fill: $gl-text-color;        } + +      &.icon-action-retry, +      &.icon-action-play { +        svg { +          width: #{$ci-action-icon-size - 6}; +          height: #{$ci-action-icon-size - 6}; +          left: 8px; +        } +      } + +      }      // link to the build @@ -762,6 +823,11 @@ button.mini-pipeline-graph-dropdown-toggle {          margin-left: 2px;          display: inline-block; +        &::after { +          content: ''; +          display: block; +        } +          @media (max-width: $screen-xs-max) {            max-width: 60%;          } @@ -799,13 +865,10 @@ button.mini-pipeline-graph-dropdown-toggle {    left: 100%;    top: -10px;    box-shadow: 0 1px 5px $black-transparent; -} - -/** - * Top arrow in the dropdown in the big pipeline graph - */ -.big-pipeline-graph-dropdown-menu { +  /** +   * Top arrow in the dropdown in the big pipeline graph +   */    &::before,    &::after {      content: ''; @@ -867,22 +930,23 @@ button.mini-pipeline-graph-dropdown-toggle {      margin-top: 1px;      border-bottom-color: $white-light;    } -} -/** - * Center dropdown menu in mini graph - */ -.mini-pipeline-graph-dropdown-menu.dropdown-menu { -  transform: translate(-80%, 0); -  min-width: 150px; +  /** +   * Center dropdown menu in mini graph +   */ +  &.dropdown-menu { +    transform: translate(-80%, 0); +    min-width: 150px; -  @media(min-width: $screen-md-min) { -    transform: translate(-50%, 0); -    right: auto; -    left: 50%; -    min-width: 240px; +    @media(min-width: $screen-md-min) { +      transform: translate(-50%, 0); +      right: auto; +      left: 50%; +      min-width: 240px; +    }    }  } +  /**   * Terminal   */ @@ -892,7 +956,7 @@ button.mini-pipeline-graph-dropdown-toggle {  .terminal-container {    .content-block { -    border-bottom: none; +    border-bottom: 0;    }    #terminal { @@ -906,25 +970,6 @@ button.mini-pipeline-graph-dropdown-toggle {    }  } -/** - * Play button with icon in dropdowns - */ -.ci-table .no-btn { -  border: none; -  background: none; -  outline: none; -  width: 100%; -  text-align: left; - -  .icon-play { -    position: relative; -    top: 2px; -    margin-right: 5px; -    height: 13px; -    width: 12px; -  } -} -  .ci-header-container {    min-height: 55px; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index eab39f698c3..28dc71dc641 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -113,7 +113,7 @@    li {      padding: 3px 0; -    border: none; +    border: 0;    }  } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a086c11324d..aaad6dbba8e 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -48,7 +48,8 @@        border: 1px solid $border-color;      } -    + .select2 a { +    + .select2 a, +    + .btn-default {        border-top-left-radius: 0;        border-bottom-left-radius: 0;      } @@ -79,7 +80,7 @@  .project-feature-settings {    background: $gray-lighter; -  border-top: none; +  border-top: 0;    margin-bottom: 16px;  } @@ -87,7 +88,8 @@    transition: background 2s ease-out;    &:disabled { -    opacity: 0.75; +    opacity: 0.5; +    pointer-events: none;    }    .highlight-changes & { @@ -126,7 +128,7 @@  .project-feature-toggle {    position: relative; -  border: none; +  border: 0;    outline: 0;    display: block;    width: 100px; @@ -481,7 +483,7 @@ a.deploy-project-label {    flex: 1;    padding: 0;    background: transparent; -  border: none; +  border: 0;    line-height: 34px;    margin: 0; @@ -549,10 +551,96 @@ a.deploy-project-label {    }  } -.project-template, +.project-template { +  > .form-group { +    margin-bottom: 0; +  } + +  .template-option { +    padding: $gl-padding $gl-padding $gl-padding ($gl-padding * 4); +    position: relative; + +    &:not(:first-child) { +      border-top: 1px solid $border-color; +    } +  } + +  .template-title { +    font-size: 16px; +  } + +  .template-description { +    margin: 6px 0 12px; +  } + +  .template-button { +    input { +      position: absolute; +      clip: rect(0, 0, 0, 0); +    } +  } + +  svg { +    position: absolute; +    left: $gl-padding; +    top: $gl-padding; +  } + +  .project-fields-form { +    display: none; + +    &.selected { +      display: block; +      padding: $gl-padding; +    } +  } + +  .template-input-group { +    position: relative; + +    @media (min-width: $screen-sm-min) { +      display: flex; +    } + +    .input-group-addon { +      flex: 1; +      text-align: left; +      padding-left: ($gl-padding * 3); +      background-color: $white-light; +    } + +    .selected-template { +      line-height: 20px; +    } + +    .selected-icon { +      svg { +        display: none; +        top: 7px; +        height: 20px; +        width: 20px; + +        &.active { +          display: block; +        } +      } +    } +  } +} + +.gitlab-tab-content { +  .import-project-pane { +    padding-bottom: 6px; +  } +} +  .project-import { -  .form-group { -    margin-bottom: 5px; +  .import-btn-container { +    margin-bottom: 0; +  } + +  .toggle-import-form { +    padding-bottom: 10px;    }    .import-buttons { @@ -567,10 +655,6 @@ a.deploy-project-label {        margin-right: 10px;      } -    .blank-option { -      min-width: 70px; -    } -      .btn-template-icon {        height: 24px;        width: inherit; @@ -592,18 +676,6 @@ a.deploy-project-label {        }      } -    .icon-rails path { -      fill: $rails; -    } - -    .icon-node-express path { -      fill: $node; -    } - -    .icon-java-spring path { -      fill: $java; -    } -      > div {        margin-bottom: 10px;        padding-left: 0; @@ -611,10 +683,6 @@ a.deploy-project-label {    }  } -.project-templates-buttons .btn:last-child { -  margin-right: 0; -} -  .create-project-options {    display: flex; @@ -711,35 +779,35 @@ a.deploy-project-label {    .nav {      padding-top: 12px;      padding-bottom: 12px; -  } -  .nav > li { -    display: inline-block; +    > li { +      display: inline-block; -    &:not(:last-child) { -      margin-right: $gl-padding; -    } +      &:not(:last-child) { +        margin-right: $gl-padding; +      } -    &.right { -      vertical-align: top; -      margin-top: 0; +      &.right { +        vertical-align: top; +        margin-top: 0; -      @media (min-width: $screen-lg-min) { -        float: right; +        @media (min-width: $screen-lg-min) { +          float: right; +        }        } -    } -  } -  .nav > li > a { -    padding: 0; -    background-color: transparent; -    font-size: 14px; -    line-height: 29px; -    color: $notes-light-color; +      > a { +        padding: 0; +        background-color: transparent; +        font-size: 14px; +        line-height: 29px; +        color: $notes-light-color; -    &:hover, -    &:focus { -      color: $gl-text-color; +        &:hover, +        &:focus { +          color: $gl-text-color; +        } +      }      }    } @@ -944,7 +1012,7 @@ pre.light-well {      margin: 0;      border-radius: 0 0 1px 1px;      padding: 20px 0; -    border: none; +    border: 0;    }    .table-bordered { @@ -1053,6 +1121,12 @@ pre.light-well {      min-width: 100px;    } +  &.form-group { +    @media (min-width: $screen-sm-min) { +      margin-bottom: 0; +    } +  } +    .select2-choice {      border-top-right-radius: 0;      border-bottom-right-radius: 0; @@ -1087,18 +1161,11 @@ pre.light-well {    }  } -.project-repo-select { -  &.disabled { -    opacity: 0.5; -    pointer-events: none; -  } -} -  .variables-table {    table-layout: fixed;    &.table-responsive { -    border: none; +    border: 0;    }    .variable-key { diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index c36fe25f74d..d93c51d5448 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -1,17 +1,3 @@ -.fade-enter-active, -.fade-leave-active { -  transition: opacity $sidebar-transition-duration; -} - -.monaco-loader { -  position: absolute; -  top: 0; -  left: 0; -  right: 0; -  bottom: 0; -  background: $black-transparent; -} -  .modal.popup-dialog {    display: block;    background-color: $black-transparent; @@ -59,6 +45,7 @@    }    .tree-content-holder { +    display: -webkit-flex;      display: flex;      min-height: 300px;    } @@ -68,14 +55,16 @@    }    .panel-right { +    display: -webkit-flex;      display: flex; +    -webkit-flex-direction: column;      flex-direction: column;      width: 80%;      height: 100%;      .monaco-editor.vs {        .current-line { -        border: none; +        border: 0;          background: $well-light-border;        } @@ -86,10 +75,6 @@            text-decoration: underline;          }        } - -      .cursor { -        display: none !important; -      }      }      .blob-no-preview { @@ -99,21 +84,12 @@        }      } -    &.edit-mode { -      .blob-viewer-container { -        overflow: hidden; -      } - -      .monaco-editor.vs { -        .cursor { -          background: $black; -          border-color: $black; -          display: block !important; -        } -      } +    &.blob-editor-container { +      overflow: hidden;      }      .blob-viewer-container { +      -webkit-flex: 1;        flex: 1;        overflow: auto; @@ -143,6 +119,7 @@      }      #tabs { +      position: relative;        flex-shrink: 0;        display: flex;        width: 100%; @@ -153,57 +130,47 @@        overflow-x: auto;        li { -        animation: swipeRightAppear ease-in 0.1s; -        animation-iteration-count: 1; -        transform-origin: 0% 50%; -        list-style-type: none; +        position: relative;          background: $gray-normal; -        display: inline-block;          padding: #{$gl-padding / 2} $gl-padding;          border-right: 1px solid $white-dark;          border-bottom: 1px solid $white-dark; -        white-space: nowrap;          cursor: pointer; -        &.remove { -          animation: swipeRightDissapear ease-in 0.1s; -          animation-iteration-count: 1; -          transform-origin: 0% 50%; - -          a { -            width: 0; -          } -        } -          &.active {            background: $white-light; -          border-bottom: none; +          border-bottom: 0;          }          a {            @include str-truncated(100px); -          color: $black; +          color: $gl-text-color;            vertical-align: middle;            text-decoration: none;            margin-right: 12px; -          &.close { -            width: auto; -            font-size: 15px; -            opacity: 1; -            margin-right: -6px; +          &:focus { +            outline: none;            }          } +        .close-btn { +          position: absolute; +          right: 8px; +          top: 50%; +          padding: 0; +          background: none; +          border: 0; +          font-size: $gl-font-size; +          transform: translateY(-50%); +        } +          .close-icon:hover {            color: $hint-color;          }          .close-icon,          .unsaved-icon { -          float: right; -          margin-top: 3px; -          margin-left: 15px;            color: $gray-darkest;          } @@ -214,17 +181,15 @@          &.tabs-divider {            width: 100%;            background-color: $white-light; -          border-right: none; +          border-right: 0;            border-top-right-radius: 2px;          }        }      } -    #repo-file-buttons { +    .repo-file-buttons {        background-color: $white-light; -      border-bottom: 1px solid $white-normal;        padding: 5px 10px; -      position: relative;        border-top: 1px solid $white-normal;      } @@ -287,37 +252,23 @@        overflow: auto;      } -    table { +    .table {        margin-bottom: 0;      }      tr { -      animation: fadein 0.5s; -      cursor: pointer; - -      &.repo-file-options td { -        padding: 0; -        border-top: none; -        background: $gray-light; +      .repo-file-options { +        padding: 2px 16px;          width: 100%; -        display: inline-block; - -        &:first-child { -          border-top-left-radius: 2px; -        } +      } -        .title { -          display: inline-block; -          font-size: 10px; -          text-transform: uppercase; -          font-weight: $gl-font-weight-bold; -          color: $gray-darkest; -          white-space: nowrap; -          overflow: hidden; -          text-overflow: ellipsis; -          vertical-align: middle; -          padding: 2px 16px; -        } +      .title { +        font-size: 10px; +        text-transform: uppercase; +        white-space: nowrap; +        overflow: hidden; +        text-overflow: ellipsis; +        vertical-align: middle;        }        .file-icon { @@ -329,11 +280,13 @@        }      } +    .file { +      cursor: pointer; +    } +      a {        @include str-truncated(250px);        color: $almost-black; -      display: inline-block; -      vertical-align: middle;      }    }  } @@ -346,22 +299,6 @@    }  } -@keyframes swipeRightAppear { -  0% { -    transform: scaleX(0.00); -  } - -  100% { -    transform: scaleX(1.00); -  } -} - -@keyframes swipeRightDissapear { -  0% { -    transform: scaleX(1.00); -  } - -  100% { -    transform: scaleX(0.00); -  } +.multi-file-table-col-name { +  width: 350px;  } diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index 6cac37a4e28..5fb97b13470 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -50,3 +50,10 @@      font-size: 11px;    }  } + +@media (max-width: $screen-md-max) { +  .runners-content { +    width: 100%; +    overflow: auto; +  } +} diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index db0a04a5eb3..fe455a04960 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -5,7 +5,7 @@      margin-bottom: $gl-padding;      &:last-child { -      border-bottom: none; +      border-bottom: 0;      }    } @@ -57,7 +57,7 @@ input[type="checkbox"]:hover {    }    .search-input { -    border: none; +    border: 0;      font-size: 14px;      padding: 0 20px 0 0;      margin-left: 5px; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 41a6ba2023a..8b9b47a41bc 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -23,15 +23,14 @@  }  .settings { -  overflow: hidden;    border-bottom: 1px solid $gray-darker;    &:first-of-type {      margin-top: 10px;    } -  &.expanded { -    overflow: visible; +  &.animating { +    overflow: hidden;    }  } @@ -56,14 +55,18 @@    overflow-y: scroll;    padding-right: 110px;    animation: collapseMaxHeight 300ms ease-out; +  // Keep the section from expanding when we scroll over it +  pointer-events: none; -  &.expanded { +  .settings.expanded & {      max-height: none;      overflow-y: visible;      animation: expandMaxHeight 300ms ease-in; +    // Reset and allow clicks again when expanded +    pointer-events: auto;    } -  &.no-animate { +  .settings.no-animate & {      animation: none;    } @@ -238,11 +241,11 @@          margin-left: 5px;          background: $badge-bg;        } -    } -    /* Ensure we don't add border if there's only single li */ -    li + li { -      border-top: 1px solid $border-color; +      /* Ensure we don't add border if there's only single li */ +      + li { +        border-top: 1px solid $border-color; +      }      }    }  } diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss index bfe065dbbaf..2bf0bedb1f5 100644 --- a/app/assets/stylesheets/pages/sherlock.scss +++ b/app/assets/stylesheets/pages/sherlock.scss @@ -5,10 +5,10 @@ table .sherlock-code {  .sherlock-code {    pre {      word-wrap: normal; -  } -  pre code { -    white-space: pre; +    code { +      white-space: pre; +    }    }  } @@ -21,13 +21,13 @@ table .sherlock-code {      text-align: right;      padding: 0 10px !important;    } + +  .slow { +    color: $red-500; +    font-weight: $gl-font-weight-bold; +  }  }  .sherlock-file-sample pre {    padding-top: 28px !important;  } - -.sherlock-line-samples-table .slow { -  color: $red-500; -  font-weight: $gl-font-weight-bold; -} diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index dfa4d033fb8..cede147d559 100644 --- a/app/assets/stylesheets/pages/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss @@ -40,16 +40,16 @@      @media (max-width: $screen-xs-max) {        width: 100%;      } -  } -  .person .spark { -    display: block; -    background: $stat-graph-common-bg; -    width: 100%; -  } +    .spark { +      display: block; +      background: $stat-graph-common-bg; +      width: 100%; +    } -  .person .area-contributor { -    fill: $stat-graph-orange-fill; +    .area-contributor { +      fill: $stat-graph-orange-fill; +    }    }  } diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 6c8d87185e9..2139a029fc7 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -141,7 +141,7 @@        }        pre { -        border: none; +        border: 0;          background: $gray-light;          border-radius: 0;          color: $todo-body-pre-color; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index e2f6e511c86..50f0ef4414a 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -252,7 +252,7 @@      margin-top: 20px;      padding: 0;      border-top: 1px solid $white-dark; -    border-bottom: none; +    border-bottom: 0;    }    .commit-stats li { diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index b7d4e7bf582..e150f96f3fa 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -161,10 +161,10 @@ ul.wiki-pages-list.content-list {      list-style: none;      margin-left: 0;      padding-left: 15px; -  } -  ul li { -    padding: 5px 0; +    li { +      padding: 5px 0; +    }    }  } diff --git a/app/assets/stylesheets/test.scss b/app/assets/stylesheets/test.scss index 06733b7f1a9..e65b49c36f3 100644 --- a/app/assets/stylesheets/test.scss +++ b/app/assets/stylesheets/test.scss @@ -4,11 +4,6 @@    -ms-transition: none !important;    -webkit-transition: none !important;    transition: none !important; -  -o-transform: none !important; -  -moz-transform: none !important; -  -ms-transform: none !important; -  -webkit-transform: none !important; -  transform: none !important;    -webkit-animation: none !important;    -moz-animation: none !important;    -o-animation: none !important; diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index fb6d8c0bb81..5be23c76a95 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -19,10 +19,12 @@ class Admin::ApplicationsController < Admin::ApplicationController    end    def create -    @application = Doorkeeper::Application.new(application_params) +    @application = Applications::CreateService.new(current_user, application_params).execute(request) -    if @application.save -      redirect_to_admin_page +    if @application.persisted? +      flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) + +      redirect_to admin_application_url(@application)      else        render :new      end @@ -41,13 +43,6 @@ class Admin::ApplicationsController < Admin::ApplicationController      redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.'    end -  protected - -  def redirect_to_admin_page -    flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) -    redirect_to admin_application_url(@application) -  end -    private    def set_application diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb index 07c8bf714fc..7a2c7234a1e 100644 --- a/app/controllers/admin/impersonation_tokens_controller.rb +++ b/app/controllers/admin/impersonation_tokens_controller.rb @@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController    end    def set_index_vars -    @scopes = Gitlab::Auth::API_SCOPES +    @scopes = Gitlab::Auth.available_scopes(current_user)      @impersonation_token ||= finder.build      @inactive_impersonation_tokens = finder(state: 'inactive').execute diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 719893c0bc8..38b808cdc31 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -2,7 +2,8 @@ class Admin::RunnersController < Admin::ApplicationController    before_action :runner, except: :index    def index -    @runners = Ci::Runner.order('id DESC') +    sort = params[:sort] == 'contacted_asc' ? { contacted_at: :asc } : { id: :desc } +    @runners = Ci::Runner.order(sort)      @runners = @runners.search(params[:search]) if params[:search].present?      @runners = @runners.page(params[:page]).per(30)      @active_runners_cnt = Ci::Runner.online.count diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 967fe39256a..3be7aee69bc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,7 +11,7 @@ class ApplicationController < ActionController::Base    include EnforcesTwoFactorAuthentication    include WithPerformanceBar -  before_action :authenticate_user_from_private_token! +  before_action :authenticate_user_from_personal_access_token!    before_action :authenticate_user_from_rss_token!    before_action :authenticate_user!    before_action :validate_user_service_ticket! @@ -100,13 +100,12 @@ class ApplicationController < ActionController::Base      return try(:authenticated_user)    end -  # This filter handles both private tokens and personal access tokens -  def authenticate_user_from_private_token! +  def authenticate_user_from_personal_access_token!      token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence      return unless token.present? -    user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) +    user = User.find_by_personal_access_token(token)      sessionless_sign_in(user)    end @@ -349,6 +348,6 @@ class ApplicationController < ActionController::Base    def set_page_title_header      # Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8 -    response.headers['Page-Title'] = page_title('GitLab').encode('ISO-8859-1') +    response.headers['Page-Title'] = URI.escape(page_title('GitLab'))    end  end diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb new file mode 100644 index 00000000000..9d4f97aa443 --- /dev/null +++ b/app/controllers/concerns/group_tree.rb @@ -0,0 +1,24 @@ +module GroupTree +  def render_group_tree(groups) +    @groups = if params[:filter].present? +                Gitlab::GroupHierarchy.new(groups.search(params[:filter])) +                  .base_and_ancestors +              else +                # Only show root groups if no parent-id is given +                groups.where(parent_id: params[:parent_id]) +              end +    @groups = @groups.with_selects_for_list(archived: params[:archived]) +                .sort(@sort = params[:sort]) +                .page(params[:page]) + +    respond_to do |format| +      format.html +      format.json do +        serializer = GroupChildSerializer.new(current_user: current_user) +                       .with_pagination(request, response) +        serializer.expand_hierarchy if params[:filter].present? +        render json: serializer.represent(@groups) +      end +    end +  end +end diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 4079072a930..072dffaff7a 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -7,14 +7,59 @@ module IssuableActions      before_action :authorize_admin_issuable!, only: :bulk_update    end +  def show +    respond_to do |format| +      format.html +      format.json do +        render json: serializer.represent(issuable, serializer: params[:serializer]) +      end +    end +  end + +  def update +    @issuable = update_service.execute(issuable) + +    respond_to do |format| +      format.html do +        recaptcha_check_with_fallback { render :edit } +      end + +      format.json do +        render_entity_json +      end +    end + +  rescue ActiveRecord::StaleObjectError +    render_conflict_response +  end + +  def realtime_changes +    Gitlab::PollingInterval.set_header(response, interval: 3_000) + +    response = { +      title: view_context.markdown_field(issuable, :title), +      title_text: issuable.title, +      description: view_context.markdown_field(issuable, :description), +      description_text: issuable.description, +      task_status: issuable.task_status +    } + +    if issuable.edited? +      response[:updated_at] = issuable.updated_at +      response[:updated_by_name] = issuable.last_edited_by.name +      response[:updated_by_path] = user_path(issuable.last_edited_by) +    end + +    render json: response +  end +    def destroy      issuable.destroy -    destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym -    TodoService.new.public_send(destroy_method, issuable, current_user) # rubocop:disable GitlabSecurity/PublicSend +    TodoService.new.destroy_issuable(issuable, current_user)      name = issuable.human_class_name      flash[:notice] = "The #{name} was successfully deleted." -    index_path = polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]) +    index_path = polymorphic_path([parent, issuable.class])      respond_to do |format|        format.html { redirect_to index_path } @@ -68,6 +113,10 @@ module IssuableActions      end    end +  def authorize_update_issuable! +    render_404 unless can?(current_user, :"update_#{resource_name}", issuable) +  end +    def bulk_update_params      permitted_keys = [        :issuable_ids, @@ -92,4 +141,24 @@ module IssuableActions    def resource_name      @resource_name ||= controller_name.singularize    end + +  def render_entity_json +    if @issuable.valid? +      render json: serializer.represent(@issuable) +    else +      render json: { errors: @issuable.errors.full_messages }, status: :unprocessable_entity +    end +  end + +  def serializer +    raise NotImplementedError +  end + +  def update_service +    raise NotImplementedError +  end + +  def parent +    @project || @group +  end  end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 3181f517087..2b011bc87b0 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -4,58 +4,44 @@ module IssuableCollections    include Gitlab::IssuableMetadata    included do -    helper_method :issues_finder -    helper_method :merge_requests_finder +    helper_method :finder    end    private -  def set_issues_index -    @collection_type    = "Issue" -    @issues             = issues_collection -    @issues             = @issues.page(params[:page]) -    @issuable_meta_data = issuable_meta_data(@issues, @collection_type) -    @total_pages        = issues_page_count(@issues) +  def set_issuables_index +    @issuables          = issuables_collection +    @issuables          = @issuables.page(params[:page]) +    @issuable_meta_data = issuable_meta_data(@issuables, collection_type) +    @total_pages        = issuable_page_count -    return if redirect_out_of_range(@issues, @total_pages) +    return if redirect_out_of_range(@total_pages)      if params[:label_name].present? -      @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute +      labels_params = { project_id: @project.id, title: params[:label_name] } +      @labels = LabelsFinder.new(current_user, labels_params).execute      end      @users = [] -  end - -  def issues_collection -    issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace) -  end - -  def merge_requests_collection -    merge_requests_finder.execute.preload( -      :source_project, -      :target_project, -      :author, -      :assignee, -      :labels, -      :milestone, -      head_pipeline: :project, -      target_project: :namespace, -      merge_request_diff: :merge_request_diff_commits -    ) -  end +    if params[:assignee_id].present? +      assignee = User.find_by_id(params[:assignee_id]) +      @users.push(assignee) if assignee +    end -  def issues_finder -    @issues_finder ||= issuable_finder_for(IssuesFinder) +    if params[:author_id].present? +      author = User.find_by_id(params[:author_id]) +      @users.push(author) if author +    end    end -  def merge_requests_finder -    @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder) +  def issuables_collection +    finder.execute.preload(preload_for_collection)    end -  def redirect_out_of_range(relation, total_pages) +  def redirect_out_of_range(total_pages)      return false if total_pages.zero? -    out_of_range = relation.current_page > total_pages +    out_of_range = @issuables.current_page > total_pages      if out_of_range        redirect_to(url_for(params.merge(page: total_pages, only_path: true))) @@ -64,12 +50,8 @@ module IssuableCollections      out_of_range    end -  def issues_page_count(relation) -    page_count_for_relation(relation, issues_finder.row_count) -  end - -  def merge_requests_page_count(relation) -    page_count_for_relation(relation, merge_requests_finder.row_count) +  def issuable_page_count +    page_count_for_relation(@issuables, finder.row_count)    end    def page_count_for_relation(relation, row_count) @@ -145,4 +127,31 @@ module IssuableCollections      else value      end    end + +  def finder +    return @finder if defined?(@finder) + +    @finder = issuable_finder_for(@finder_type) +  end + +  def collection_type +    @collection_type ||= case finder +                         when IssuesFinder +                           'Issue' +                         when MergeRequestsFinder +                           'MergeRequest' +                         end +  end + +  def preload_for_collection +    @preload_for_collection ||= case collection_type +                                when 'Issue' +                                  [:project, :author, :assignees, :labels, :milestone, project: :namespace] +                                when 'MergeRequest' +                                  [ +                                    :source_project, :target_project, :author, :assignee, :labels, :milestone, +                                    head_pipeline: :project, target_project: :namespace, merge_request_diff: :merge_request_diff_commits +                                  ] +                                end +  end  end diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index 404559c8707..ad594903331 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -3,14 +3,14 @@ module IssuesAction    include IssuableCollections    def issues -    @label = issues_finder.labels.first +    @finder_type = IssuesFinder +    @label = finder.labels.first -    @issues = issues_collection +    @issues = issuables_collection                .non_archived                .page(params[:page]) -    @collection_type    = "Issue" -    @issuable_meta_data = issuable_meta_data(@issues, @collection_type) +    @issuable_meta_data = issuable_meta_data(@issues, collection_type)      respond_to do |format|        format.html diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb index 2b6afaa6233..738afd612f0 100644 --- a/app/controllers/concerns/lfs_request.rb +++ b/app/controllers/concerns/lfs_request.rb @@ -94,10 +94,9 @@ module LfsRequest      @storage_project ||= begin        result = project -      loop do -        break unless result.forked? -        result = result.forked_from_project -      end +      # TODO: Make this go to the fork_network root immeadiatly +      # dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769 +      result = result.fork_source while result.forked?        result      end diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index d3c8e4888bc..8b569a01afd 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -3,13 +3,12 @@ module MergeRequestsAction    include IssuableCollections    def merge_requests -    @label = merge_requests_finder.labels.first +    @finder_type = MergeRequestsFinder +    @label = finder.labels.first -    @merge_requests = merge_requests_collection -                      .page(params[:page]) +    @merge_requests = issuables_collection.page(params[:page]) -    @collection_type    = "MergeRequest" -    @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) +    @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)    end    private diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 1126f706393..3c64fd964ff 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -4,6 +4,7 @@ module NotesActions    included do      before_action :set_polling_interval_header, only: [:index] +    before_action :noteable, only: :index      before_action :authorize_admin_note!, only: [:update, :destroy]      before_action :note_project, only: [:create]    end @@ -38,7 +39,7 @@ module NotesActions      @note = Notes::CreateService.new(note_project, current_user, create_params).execute      if @note.is_a?(Note) -      Banzai::NoteRenderer.render([@note], @project, current_user) +      Notes::RenderService.new(current_user).execute([@note], @project)      end      respond_to do |format| @@ -51,7 +52,7 @@ module NotesActions      @note = Notes::UpdateService.new(project, current_user, note_params).execute(note)      if @note.is_a?(Note) -      Banzai::NoteRenderer.render([@note], @project, current_user) +      Notes::RenderService.new(current_user).execute([@note], @project)      end      respond_to do |format| @@ -108,6 +109,8 @@ module NotesActions              diff_discussion_html: diff_discussion_html(discussion),              discussion_html: discussion_html(discussion)            ) + +          attrs[:discussion_line_code] = discussion.line_code if discussion.diff_discussion?          end        end      else @@ -188,7 +191,7 @@ module NotesActions    end    def noteable -    @noteable ||= notes_finder.target +    @noteable ||= notes_finder.target || render_404    end    def last_fetched_at diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb new file mode 100644 index 00000000000..5ce602b55a8 --- /dev/null +++ b/app/controllers/concerns/preview_markdown.rb @@ -0,0 +1,22 @@ +module PreviewMarkdown +  extend ActiveSupport::Concern + +  def preview_markdown +    result = PreviewMarkdownService.new(@project, current_user, params).execute + +    markdown_params = +      case controller_name +      when 'wikis'    then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] } +      when 'snippets' then { skip_project_check: true } +      else {} +      end + +    render json: { +      body: view_context.markdown(result[:text], markdown_params), +      references: { +        users: result[:users], +        commands: view_context.markdown(result[:commands]) +      } +    } +  end +end diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb index 4791bc561a4..824ad06465c 100644 --- a/app/controllers/concerns/renders_notes.rb +++ b/app/controllers/concerns/renders_notes.rb @@ -3,7 +3,7 @@ module RendersNotes      preload_noteable_for_regular_notes(notes)      preload_max_access_for_authors(notes, @project)      preload_first_time_contribution_for_authors(noteable, notes) -    Banzai::NoteRenderer.render(notes, @project, current_user) +    Notes::RenderService.new(current_user).execute(notes, @project)      notes    end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 80ab681ed87..bc0948cd3fb 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -10,7 +10,7 @@ class ConfirmationsController < Devise::ConfirmationsController      users_almost_there_path    end -  def after_confirmation_path_for(_resource_name, resource) +  def after_confirmation_path_for(resource_name, resource)      # incoming resource can either be a :user or an :email      if signed_in?(:user)        after_sign_in(resource) diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb index 7ed18fb481c..025769f512a 100644 --- a/app/controllers/dashboard/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,33 +1,8 @@  class Dashboard::GroupsController < Dashboard::ApplicationController -  def index -    @sort = params[:sort] || 'created_desc' - -    @groups = -      if params[:parent_id] && Group.supports_nested_groups? -        parent = Group.find_by(id: params[:parent_id]) - -        if can?(current_user, :read_group, parent) -          GroupsFinder.new(current_user, parent: parent).execute -        else -          Group.none -        end -      else -        current_user.groups -      end +  include GroupTree -    @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present? -    @groups = @groups.includes(:route) -    @groups = @groups.sort(@sort) -    @groups = @groups.page(params[:page]) - -    respond_to do |format| -      format.html -      format.json do -        render json: GroupSerializer -          .new(current_user: @current_user) -          .with_pagination(request, response) -          .represent(@groups) -      end -    end +  def index +    groups = GroupsFinder.new(current_user, all_available: false).execute +    render_group_tree(groups)    end  end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index cd94a36a6e7..d9884a47ec4 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -57,5 +57,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController      @events = EventCollection        .new(projects, offset: params[:offset].to_i, filter: event_filter)        .to_a + +    Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)    end  end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 19a5db6fd17..280ed93faf8 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -32,6 +32,8 @@ class DashboardController < Dashboard::ApplicationController      @events = EventCollection        .new(projects, offset: params[:offset].to_i, filter: @event_filter)        .to_a + +    Events::RenderService.new(current_user).execute(@events)    end    def set_show_full_reference diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb index 81883c543ba..fa0a0f68fbc 100644 --- a/app/controllers/explore/groups_controller.rb +++ b/app/controllers/explore/groups_controller.rb @@ -1,17 +1,7 @@  class Explore::GroupsController < Explore::ApplicationController -  def index -    @groups = GroupsFinder.new(current_user).execute -    @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present? -    @groups = @groups.sort(@sort = params[:sort]) -    @groups = @groups.page(params[:page]) +  include GroupTree -    respond_to do |format| -      format.html -      format.json do -        render json: { -          html: view_to_html_string("explore/groups/_groups", locals: { groups: @groups }) -        } -      end -    end +  def index +    render_group_tree GroupsFinder.new(current_user).execute    end  end diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb new file mode 100644 index 00000000000..b474f5d15ee --- /dev/null +++ b/app/controllers/groups/children_controller.rb @@ -0,0 +1,39 @@ +module Groups +  class ChildrenController < Groups::ApplicationController +    before_action :group + +    def index +      parent = if params[:parent_id].present? +                 GroupFinder.new(current_user).execute(id: params[:parent_id]) +               else +                 @group +               end + +      if parent.nil? +        render_404 +        return +      end + +      setup_children(parent) + +      respond_to do |format| +        format.json do +          serializer = GroupChildSerializer +                         .new(current_user: current_user) +                         .with_pagination(request, response) +          serializer.expand_hierarchy(parent) if params[:filter].present? +          render json: serializer.represent(@children) +        end +      end +    end + +    protected + +    def setup_children(parent) +      @children = GroupDescendantsFinder.new(current_user: current_user, +                                             parent_group: parent, +                                             params: params).execute +      @children = @children.page(params[:page]) +    end +  end +end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 3769a2cde33..eb53a522f90 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -2,6 +2,7 @@ class GroupsController < Groups::ApplicationController    include IssuesAction    include MergeRequestsAction    include ParamsBackwardCompatibility +  include PreviewMarkdown    respond_to :html @@ -45,15 +46,11 @@ class GroupsController < Groups::ApplicationController    end    def show -    setup_projects -      respond_to do |format| -      format.html - -      format.json do -        render json: { -          html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) -        } +      format.html do +        @has_children = GroupDescendantsFinder.new(current_user: current_user, +                                                   parent_group: @group, +                                                   params: params).has_children?        end        format.atom do @@ -63,13 +60,6 @@ class GroupsController < Groups::ApplicationController      end    end -  def subgroups -    return not_found unless Group.supports_nested_groups? - -    @nested_groups = GroupsFinder.new(current_user, parent: group).execute -    @nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present? -  end -    def activity      respond_to do |format|        format.html @@ -106,20 +96,6 @@ class GroupsController < Groups::ApplicationController    protected -  def setup_projects -    set_non_archived_param -    params[:sort] ||= 'latest_activity_desc' -    @sort = params[:sort] - -    options = {} -    options[:only_owned] = true if params[:shared] == '0' -    options[:only_shared] = true if params[:shared] == '1' - -    @projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user).execute -    @projects = @projects.includes(:namespace) -    @projects = @projects.page(params[:page]) if params[:name].blank? -  end -    def authorize_create_group!      allowed = if params[:parent_id].present?                  parent = Group.find_by(id: params[:parent_id]) @@ -165,9 +141,22 @@ class GroupsController < Groups::ApplicationController    end    def load_events +    params[:sort] ||= 'latest_activity_desc' + +    options = {} +    options[:only_owned] = true if params[:shared] == '0' +    options[:only_shared] = true if params[:shared] == '1' + +    @projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user) +                  .execute +                  .includes(:namespace) +                  .page(params[:page]) +      @events = EventCollection        .new(@projects, offset: params[:offset].to_i, filter: event_filter)        .to_a + +    Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)    end    def user_actions diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 572915a4930..38f379dbf4f 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -57,6 +57,10 @@ class HelpController < ApplicationController    def shortcuts    end +  def instance_configuration +    @instance_configuration = InstanceConfiguration.new +  end +    def ui      @user = User.new(id: 0, name: 'John Doe', username: '@johndoe')    end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 4bceb1d67a3..7d6fe6a0232 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -30,11 +30,11 @@ class JwtController < ApplicationController          render_unauthorized        end      end -  rescue Gitlab::Auth::MissingPersonalTokenError -    render_missing_personal_token +  rescue Gitlab::Auth::MissingPersonalAccessTokenError +    render_missing_personal_access_token    end -  def render_missing_personal_token +  def render_missing_personal_access_token      render json: {        errors: [          { code: 'UNAUTHORIZED', diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index 37587a52eaf..d81ad135198 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -3,10 +3,16 @@ class MetricsController < ActionController::Base    protect_from_forgery with: :exception -  before_action :validate_prometheus_metrics -    def index -    render text: metrics_service.metrics_text, content_type: 'text/plain; version=0.0.4' +    response = if Gitlab::Metrics.prometheus_metrics_enabled? +                 metrics_service.metrics_text +               else +                 help_page = help_page_url('administration/monitoring/prometheus/gitlab_metrics', +                                           anchor: 'gitlab-prometheus-metrics' +                                          ) +                 "# Metrics are disabled, see: #{help_page}\n" +               end +    render text: response, content_type: 'text/plain; version=0.0.4'    end    private @@ -14,8 +20,4 @@ class MetricsController < ActionController::Base    def metrics_service      @metrics_service ||= MetricsService.new    end - -  def validate_prometheus_metrics -    render_404 unless Gitlab::Metrics.prometheus_metrics_enabled? -  end  end diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index b02e64a132b..2443f529c7b 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -16,25 +16,18 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController    end    def create -    @application = Doorkeeper::Application.new(application_params) +    @application = Applications::CreateService.new(current_user, create_application_params).execute(request) -    @application.owner = current_user +    if @application.persisted? +      flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) -    if @application.save -      redirect_to_oauth_application_page +      redirect_to oauth_application_url(@application)      else        set_index_vars        render :index      end    end -  protected - -  def redirect_to_oauth_application_page -    flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) -    redirect_to oauth_application_url(@application) -  end -    private    def verify_user_oauth_applications_enabled @@ -61,4 +54,10 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController    rescue_from ActiveRecord::RecordNotFound do |exception|      render "errors/not_found", layout: "errors", status: 404    end + +  def create_application_params +    application_params.tap do |params| +      params[:owner] = current_user +    end +  end  end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index 069e6a810f2..f0e5d2aa94e 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -11,10 +11,10 @@ class Profiles::KeysController < Profiles::ApplicationController    end    def create -    @key = Keys::CreateService.new(current_user, key_params).execute +    @key = Keys::CreateService.new(current_user, key_params.merge(ip_address: request.remote_ip)).execute      if @key.persisted? -      redirect_to_profile_key_path +      redirect_to profile_key_path(@key)      else        @keys = current_user.keys.select(&:persisted?)        render :index @@ -50,12 +50,6 @@ class Profiles::KeysController < Profiles::ApplicationController      end    end -  protected - -  def redirect_to_profile_key_path -    redirect_to profile_key_path(@key) -  end -    private    def key_params diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 4146deefa89..6d9873e38df 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController    end    def set_index_vars -    @scopes = Gitlab::Auth.available_scopes +    @scopes = Gitlab::Auth.available_scopes(current_user)      @inactive_personal_access_tokens = finder(state: 'inactive').execute      @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 5d87037f012..dbf61a17724 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -24,16 +24,6 @@ class ProfilesController < Profiles::ApplicationController      end    end -  def reset_private_token -    Users::UpdateService.new(current_user, user: @user).execute! do |user| -      user.reset_authentication_token! -    end - -    flash[:notice] = "Private token was successfully reset" - -    redirect_to profile_account_path -  end -    def reset_incoming_email_token      Users::UpdateService.new(current_user, user: @user).execute! do |user|        user.reset_incoming_email_token! @@ -41,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController      flash[:notice] = "Incoming email token was successfully reset" -    redirect_to profile_account_path +    redirect_to profile_personal_access_tokens_path    end    def reset_rss_token @@ -51,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController      flash[:notice] = "RSS token was successfully reset" -    redirect_to profile_account_path +    redirect_to profile_personal_access_tokens_path    end    def audit_log diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index d7dd8ddcb7d..9e79852e378 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -2,7 +2,6 @@ class Projects::ApplicationController < ApplicationController    include RoutableActions    skip_before_action :authenticate_user! -  before_action :redirect_git_extension    before_action :project    before_action :repository    layout 'project' @@ -11,15 +10,6 @@ class Projects::ApplicationController < ApplicationController    private -  def redirect_git_extension -    # Redirect from -    #   localhost/group/project.git -    # to -    #   localhost/group/project -    # -    redirect_to url_for(params.merge(format: nil)) if params[:format] == 'git' -  end -    def project      return @project if @project      return nil unless params[:project_id] || params[:id] diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 183a6f88a6a..770381472c5 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -205,6 +205,7 @@ class Projects::BlobController < Projects::ApplicationController      tree_path = path_segments.join('/')      render json: json.merge( +      id: @blob.id,        path: blob.path,        name: blob.name,        extension: blob.extension, diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 7f03ce07dec..f28df83d5a5 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -15,6 +15,8 @@ class Projects::BranchesController < Projects::ApplicationController      respond_to do |format|        format.html do          @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) +        @merged_branch_names = +          repository.merged_branch_names(@branches.map(&:name))          # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429          Gitlab::GitalyClient.allow_n_plus_1_calls do            @max_commits = @branches.reduce(0) do |memo, branch| diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb new file mode 100644 index 00000000000..90c7fa62216 --- /dev/null +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -0,0 +1,25 @@ +class Projects::Clusters::ApplicationsController < Projects::ApplicationController +  before_action :cluster +  before_action :application_class, only: [:create] +  before_action :authorize_read_cluster! +  before_action :authorize_create_cluster!, only: [:create] + +  def create +    Clusters::Applications::ScheduleInstallationService.new(project, current_user, +                                                            application_class: @application_class, +                                                            cluster: @cluster).execute +    head :no_content +  rescue StandardError +    head :bad_request +  end + +  private + +  def cluster +    @cluster ||= project.clusters.find(params[:id]) || render_404 +  end + +  def application_class +    @application_class ||= Clusters::Cluster::APPLICATIONS[params[:application]] || render_404 +  end +end diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 03019b0becc..9a56c9de858 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -1,8 +1,8 @@  class Projects::ClustersController < Projects::ApplicationController -  before_action :cluster, except: [:login, :index, :new, :create] +  before_action :cluster, except: [:login, :index, :new, :new_gcp, :create]    before_action :authorize_read_cluster! -  before_action :authorize_create_cluster!, only: [:new, :create] -  before_action :authorize_google_api, only: [:new, :create] +  before_action :authorize_create_cluster!, only: [:new, :new_gcp, :create] +  before_action :authorize_google_api, only: [:new_gcp, :create]    before_action :authorize_update_cluster!, only: [:update]    before_action :authorize_admin_cluster!, only: [:destroy] @@ -16,7 +16,7 @@ class Projects::ClustersController < Projects::ApplicationController    def login      begin -      state = generate_session_key_redirect(namespace_project_clusters_url.to_s) +      state = generate_session_key_redirect(providers_gcp_new_namespace_project_clusters_url.to_s)        @authorize_url = GoogleApi::CloudPlatform::Client.new(          nil, callback_google_api_auth_url, @@ -27,18 +27,23 @@ class Projects::ClustersController < Projects::ApplicationController    end    def new -    @cluster = project.build_cluster +  end + +  def new_gcp +    @cluster = Clusters::Cluster.new.tap do |cluster| +      cluster.build_provider_gcp +    end    end    def create -    @cluster = Ci::CreateClusterService +    @cluster = Clusters::CreateService        .new(project, current_user, create_params)        .execute(token_in_session)      if @cluster.persisted?        redirect_to project_cluster_path(project, @cluster)      else -      render :new +      render :new_gcp      end    end @@ -58,7 +63,7 @@ class Projects::ClustersController < Projects::ApplicationController    end    def update -    Ci::UpdateClusterService +    Clusters::UpdateService        .new(project, current_user, update_params)        .execute(cluster) @@ -88,19 +93,19 @@ class Projects::ClustersController < Projects::ApplicationController    def create_params      params.require(:cluster).permit( -      :gcp_project_id, -      :gcp_cluster_zone, -      :gcp_cluster_name, -      :gcp_cluster_size, -      :gcp_machine_type, -      :project_namespace, -      :enabled) +      :enabled, +      :name, +      :provider_type, +      provider_gcp_attributes: [ +        :gcp_project_id, +        :zone, +        :num_nodes, +        :machine_type +      ])    end    def update_params -    params.require(:cluster).permit( -      :project_namespace, -      :enabled) +    params.require(:cluster).permit(:enabled)    end    def authorize_google_api diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index a62f05db7db..494d412b532 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -16,6 +16,8 @@ class Projects::CommitController < Projects::ApplicationController    before_action :define_note_vars, only: [:show, :diff_for_path]    before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] +  BRANCH_SEARCH_LIMIT = 1000 +    def show      apply_diff_view_cookie! @@ -56,8 +58,14 @@ class Projects::CommitController < Projects::ApplicationController    end    def branches -    @branches = @project.repository.branch_names_contains(commit.id) -    @tags = @project.repository.tag_names_contains(commit.id) +    # branch_names_contains/tag_names_contains can take a long time when there are thousands of +    # branches/tags - each `git branch --contains xxx` request can consume a cpu core. +    # so only do the query when there are a manageable number of branches/tags +    @branches_limit_exceeded = @project.repository.branch_count > BRANCH_SEARCH_LIMIT +    @branches = @branches_limit_exceeded ? [] : @project.repository.branch_names_contains(commit.id) + +    @tags_limit_exceeded = @project.repository.tag_count > BRANCH_SEARCH_LIMIT +    @tags = @tags_limit_exceeded ? [] : @project.repository.tag_names_contains(commit.id)      render layout: false    end diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 4a841bf2073..d48284a4429 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -48,6 +48,8 @@ class Projects::CommitsController < Projects::ApplicationController    private    def set_commits +    render_404 unless request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present? +      @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i      search = params[:search] diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 95d7a02e9e9..dd5e66f60e3 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -53,8 +53,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController      send_challenges      render plain: "HTTP Basic: Access denied\n", status: 401 -  rescue Gitlab::Auth::MissingPersonalTokenError -    render_missing_personal_token +  rescue Gitlab::Auth::MissingPersonalAccessTokenError +    render_missing_personal_access_token    end    def basic_auth_provided? @@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController      @project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}")    end -  def render_missing_personal_token +  def render_missing_personal_access_token      render plain: "HTTP Basic: Access denied\n" \                    "You must use a personal access token with 'api' scope for Git over HTTP.\n" \                    "You can generate one at #{profile_personal_access_tokens_url}", diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index f59200d3b1f..dbc1c8bcc28 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -12,12 +12,7 @@ class Projects::GroupLinksController < Projects::ApplicationController      if group        return render_404 unless can?(current_user, :read_group, group) - -      project.project_group_links.create( -        group: group, -        group_access: params[:link_group_access], -        expires_at: params[:expires_at] -      ) +      Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group)      else        flash[:alert] = 'Please select a group.'      end @@ -32,7 +27,9 @@ class Projects::GroupLinksController < Projects::ApplicationController    end    def destroy -    project.project_group_links.find(params[:id]).destroy +    group_link = project.project_group_links.find(params[:id]) + +    ::Projects::GroupLinks::DestroyService.new(project, current_user).execute(group_link)      respond_to do |format|        format.html do @@ -47,4 +44,8 @@ class Projects::GroupLinksController < Projects::ApplicationController    def group_link_params      params.require(:group_link).permit(:group_access, :expires_at)    end + +  def group_link_create_params +    params.permit(:link_group_access, :expires_at) +  end  end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b7a108a0ebd..dbc9106ba6d 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -10,13 +10,13 @@ class Projects::IssuesController < Projects::ApplicationController    before_action :check_issues_available!    before_action :issue, except: [:index, :new, :create, :bulk_update] -  before_action :set_issues_index, only: [:index] +  before_action :set_issuables_index, only: [:index]    # Allow write(create) issue    before_action :authorize_create_issue!, only: [:new, :create]    # Allow modify issue -  before_action :authorize_update_issue!, only: [:update, :move] +  before_action :authorize_update_issuable!, only: [:edit, :update, :move]    # Allow create a new branch and empty WIP merge request from current issue    before_action :authorize_create_merge_request!, only: [:create_merge_request] @@ -24,15 +24,7 @@ class Projects::IssuesController < Projects::ApplicationController    respond_to :html    def index -    if params[:assignee_id].present? -      assignee = User.find_by_id(params[:assignee_id]) -      @users.push(assignee) if assignee -    end - -    if params[:author_id].present? -      author = User.find_by_id(params[:author_id]) -      @users.push(author) if author -    end +    @issues = @issuables      respond_to do |format|        format.html @@ -63,16 +55,8 @@ class Projects::IssuesController < Projects::ApplicationController      respond_with(@issue)    end -  def show -    @noteable = @issue -    @note     = @project.notes.new(noteable: @issue) - -    respond_to do |format| -      format.html -      format.json do -        render json: serializer.represent(@issue) -      end -    end +  def edit +    respond_with(@issue)    end    def discussions @@ -116,21 +100,6 @@ class Projects::IssuesController < Projects::ApplicationController      end    end -  def update -    update_params = issue_params.merge(spammable_params) - -    @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) - -    respond_to do |format| -      format.json do -        render_issue_json -      end -    end - -  rescue ActiveRecord::StaleObjectError -    render_conflict_response -  end -    def move      params.require(:move_to_project_id) @@ -188,26 +157,6 @@ class Projects::IssuesController < Projects::ApplicationController      end    end -  def realtime_changes -    Gitlab::PollingInterval.set_header(response, interval: 3_000) - -    response = { -      title: view_context.markdown_field(@issue, :title), -      title_text: @issue.title, -      description: view_context.markdown_field(@issue, :description), -      description_text: @issue.description, -      task_status: @issue.task_status -    } - -    if @issue.edited? -      response[:updated_at] = @issue.updated_at -      response[:updated_by_name] = @issue.last_edited_by.name -      response[:updated_by_path] = user_path(@issue.last_edited_by) -    end - -    render json: response -  end -    def create_merge_request      result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute @@ -223,7 +172,8 @@ class Projects::IssuesController < Projects::ApplicationController    def issue      return @issue if defined?(@issue)      # The Sortable default scope causes performance issues when used with find_by -    @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! +    @issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! +    @note = @project.notes.new(noteable: @issuable)      return render_404 unless can?(current_user, :read_issue, @issue) @@ -238,14 +188,6 @@ class Projects::IssuesController < Projects::ApplicationController      project_issue_path(@project, @issue)    end -  def authorize_update_issue! -    render_404 unless can?(current_user, :update_issue, @issue) -  end - -  def authorize_admin_issues! -    render_404 unless can?(current_user, :admin_issue, @project) -  end -    def authorize_create_merge_request!      render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)    end @@ -297,4 +239,14 @@ class Projects::IssuesController < Projects::ApplicationController    def serializer      IssueSerializer.new(current_user: current_user, project: issue.project)    end + +  def update_service +    update_params = issue_params.merge(spammable_params) +    Issues::UpdateService.new(project, current_user, update_params) +  end + +  def set_issuables_index +    @finder_type = IssuesFinder +    super +  end  end diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 0e71977a58a..1269759fc2b 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -2,7 +2,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont    before_action :check_merge_requests_available!    before_action :merge_request    before_action :authorize_read_merge_request! -  before_action :ensure_ref_fetched    private @@ -10,12 +9,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont      @issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])    end -  # Make sure merge requests created before 8.0 -  # have head file in refs/merge-requests/ -  def ensure_ref_fetched -    @merge_request.ensure_ref_fetched if Gitlab::Database.read_write? -  end -    def merge_request_params      params.require(:merge_request).permit(merge_request_params_attributes)    end diff --git a/app/controllers/projects/merge_requests/conflicts_controller.rb b/app/controllers/projects/merge_requests/conflicts_controller.rb index 28afef101a9..366524b0783 100644 --- a/app/controllers/projects/merge_requests/conflicts_controller.rb +++ b/app/controllers/projects/merge_requests/conflicts_controller.rb @@ -53,7 +53,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap        flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'        render json: { redirect_to: project_merge_request_url(@project, @merge_request, resolved_conflicts: true) } -    rescue Gitlab::Conflict::ResolutionError => e +    rescue Gitlab::Git::Conflict::Resolver::ResolutionError => e        render status: :bad_request, json: { message: e.message }      end    end diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 99dc3dda9e7..129682f64aa 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -4,7 +4,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap    include RendersCommits    skip_before_action :merge_request -  skip_before_action :ensure_ref_fetched    before_action :authorize_create_merge_request!    before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]    before_action :build_merge_request, except: [:create] diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index c5204080333..402420b851e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -7,37 +7,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo    include IssuableCollections    skip_before_action :merge_request, only: [:index, :bulk_update] -  skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update] -  before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort] +  before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] + +  before_action :set_issuables_index, only: [:index]    before_action :authenticate_user!, only: [:assign_related_issues]    def index -    @collection_type    = "MergeRequest" -    @merge_requests     = merge_requests_collection -    @merge_requests     = @merge_requests.page(params[:page]) -    @merge_requests     = @merge_requests.preload(merge_request_diff: :merge_request) -    @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) -    @total_pages        = merge_requests_page_count(@merge_requests) - -    return if redirect_out_of_range(@merge_requests, @total_pages) - -    if params[:label_name].present? -      labels_params = { project_id: @project.id, title: params[:label_name] } -      @labels = LabelsFinder.new(current_user, labels_params).execute -    end - -    @users = [] -    if params[:assignee_id].present? -      assignee = User.find_by_id(params[:assignee_id]) -      @users.push(assignee) if assignee -    end - -    if params[:author_id].present? -      author = User.find_by_id(params[:author_id]) -      @users.push(author) if author -    end +    @merge_requests = @issuables      respond_to do |format|        format.html @@ -52,7 +30,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo    def show      validates_merge_request -    ensure_ref_fetched      close_merge_request_without_source_project      check_if_can_be_merged @@ -83,7 +60,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo        format.json do          Gitlab::PollingInterval.set_header(response, interval: 10_000) -        render json: serializer.represent(@merge_request, basic: params[:basic]) +        render json: serializer.represent(@merge_request, serializer: params[:serializer])        end        format.patch  do @@ -256,14 +233,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo    alias_method :issuable, :merge_request    alias_method :awardable, :merge_request -  def authorize_update_merge_request! -    return render_404 unless can?(current_user, :update_merge_request, @merge_request) -  end - -  def authorize_admin_merge_request! -    return render_404 unless can?(current_user, :admin_merge_request, @merge_request) -  end -    def validates_merge_request      # Show git not found page      # if there is no saved commits between source & target branch @@ -348,4 +317,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo      @target_project = @merge_request.target_project      @target_branches = @merge_request.target_project.repository.branch_names    end + +  def set_issuables_index +    @finder_type = MergeRequestsFinder +    super +  end  end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index c94384d2a1a..980bbf699b6 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -2,13 +2,13 @@ class Projects::MilestonesController < Projects::ApplicationController    include MilestoneActions    before_action :check_issuables_available! -  before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels] +  before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels, :promote]    # Allow read any milestone    before_action :authorize_read_milestone!    # Allow admin milestone -  before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels] +  before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels, :promote]    respond_to :html @@ -69,6 +69,14 @@ class Projects::MilestonesController < Projects::ApplicationController      end    end +  def promote +    promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone) +    flash[:notice] = "Milestone has been promoted to group milestone." +    redirect_to group_milestone_path(project.group, promoted_milestone.iid) +  rescue Milestones::PromoteService::PromoteMilestoneError => error +    redirect_to milestone, alert: error.message +  end +    def destroy      return access_denied! unless can?(current_user, :admin_milestone, @project) diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 2fd015df688..2376f469213 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -56,9 +56,12 @@ class Projects::RefsController < Projects::ApplicationController        contents[@offset, @limit].to_a.map do |content|          file = @path ? File.join(@path, content.name) : content.name          last_commit = @repo.last_commit_for_path(@commit.id, file) +        commit_path = project_commit_path(@project, last_commit) if last_commit          {            file_name: content.name, -          commit: last_commit +          commit: last_commit, +          type: content.type, +          commit_path: commit_path          }        end      end @@ -70,6 +73,11 @@ class Projects::RefsController < Projects::ApplicationController      respond_to do |format|        format.html { render_404 } +      format.json do +        response.headers["More-Logs-Url"] = @more_log_url + +        render json: @logs +      end        format.js      end    end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index a8ebdf5a4a9..f7a9c98629d 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -1,4 +1,6 @@  class Projects::WikisController < Projects::ApplicationController +  include PreviewMarkdown +    before_action :authorize_read_wiki!    before_action :authorize_create_wiki!, only: [:edit, :create, :history]    before_action :authorize_admin_wiki!, only: :destroy @@ -92,17 +94,6 @@ class Projects::WikisController < Projects::ApplicationController    def git_access    end -  def preview_markdown -    result = PreviewMarkdownService.new(@project, current_user, params).execute - -    render json: { -      body: view_context.markdown(result[:text], pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]), -      references: { -        users: result[:users] -      } -    } -  end -    private    def load_project_wiki diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a738ca9f361..2a473ec0cec 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,8 +1,10 @@  class ProjectsController < Projects::ApplicationController    include IssuableCollections    include ExtractsPath +  include PreviewMarkdown    before_action :authenticate_user!, except: [:index, :show, :activity, :refs] +  before_action :redirect_git_extension, only: [:show]    before_action :project, except: [:index, :new, :create]    before_action :repository, except: [:index, :new, :create]    before_action :assign_ref_vars, only: [:show], if: :repo_exists? @@ -124,7 +126,7 @@ class ProjectsController < Projects::ApplicationController      return access_denied! unless can?(current_user, :remove_project, @project)      ::Projects::DestroyService.new(@project, current_user, {}).async_execute -    flash[:alert] = _("Project '%{project_name}' will be deleted.") % { project_name: @project.name_with_namespace } +    flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.name_with_namespace }      redirect_to dashboard_projects_path, status: 302    rescue Projects::DestroyService::DestroyError => ex @@ -258,18 +260,6 @@ class ProjectsController < Projects::ApplicationController      render json: options.to_json    end -  def preview_markdown -    result = PreviewMarkdownService.new(@project, current_user, params).execute - -    render json: { -      body: view_context.markdown(result[:text]), -      references: { -        users: result[:users], -        commands: view_context.markdown(result[:commands]) -      } -    } -  end -    private    # Render project landing depending of which features are available @@ -285,7 +275,8 @@ class ProjectsController < Projects::ApplicationController          @project_wiki = @project.wiki          @wiki_home = @project_wiki.find_page('home', params[:version_id])        elsif @project.feature_available?(:issues, current_user) -        @issues = issues_collection.page(params[:page]) +        @finder_type = IssuesFinder +        @issues = issuables_collection.page(params[:page])          @collection_type = 'Issue'          @issuable_meta_data = issuable_meta_data(@issues, @collection_type)        end @@ -310,6 +301,8 @@ class ProjectsController < Projects::ApplicationController      @events = EventCollection        .new(projects, offset: params[:offset].to_i, filter: event_filter)        .to_a + +    Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)    end    def project_params @@ -400,4 +393,13 @@ class ProjectsController < Projects::ApplicationController    def project_export_enabled      render_404 unless current_application_settings.project_export_enabled?    end + +  def redirect_git_extension +    # Redirect from +    #   localhost/group/project.git +    # to +    #   localhost/group/project +    # +    redirect_to request.original_url.sub(/\.git\/?\Z/, '') if params[:format] == 'git' +  end  end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index c1cdc7c9831..be2d3f638ff 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -4,6 +4,7 @@ class SnippetsController < ApplicationController    include SpammableActions    include SnippetsActions    include RendersBlob +  include PreviewMarkdown    before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] @@ -87,17 +88,6 @@ class SnippetsController < ApplicationController      redirect_to snippets_path, status: 302    end -  def preview_markdown -    result = PreviewMarkdownService.new(@project, current_user, params).execute - -    render json: { -      body: view_context.markdown(result[:text], skip_project_check: true), -      references: { -        users: result[:users] -      } -    } -  end -    protected    def snippet diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 4ee855806ab..5fca31b4956 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -108,6 +108,8 @@ class UsersController < ApplicationController        .references(:project)        .with_associations        .limit_recent(20, params[:offset]) + +    Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)    end    def load_projects diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb index 533076585c0..852eac3647d 100644 --- a/app/finders/branches_finder.rb +++ b/app/finders/branches_finder.rb @@ -23,7 +23,7 @@ class BranchesFinder    def filter_by_name(branches)      if search -      branches.select { |branch| branch.name.include?(search) } +      branches.select { |branch| branch.name.upcase.include?(search.upcase) }      else        branches      end diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb new file mode 100644 index 00000000000..1a5f6063437 --- /dev/null +++ b/app/finders/group_descendants_finder.rb @@ -0,0 +1,153 @@ +# GroupDescendantsFinder +# +# Used to find and filter all subgroups and projects of a passed parent group +# visible to a specified user. +# +# When passing a `filter` param, the search is performed over all nested levels +# of the `parent_group`. All ancestors for a search result are loaded +# +# Arguments: +#   current_user: The user for which the children should be visible +#   parent_group: The group to find children of +#   params: +#     Supports all params that the `ProjectsFinder` and `GroupProjectsFinder` +#     support. +# +#     filter: string - is aliased to `search` for consistency with the frontend +#     archived: string - `only` or `true`. +#                        `non_archived` is passed to the `ProjectFinder`s if none +#                        was given. +class GroupDescendantsFinder +  attr_reader :current_user, :parent_group, :params + +  def initialize(current_user: nil, parent_group:, params: {}) +    @current_user = current_user +    @parent_group = parent_group +    @params = params.reverse_merge(non_archived: params[:archived].blank?) +  end + +  def execute +    # The children array might be extended with the ancestors of projects when +    # filtering. In that case, take the maximum so the array does not get limited +    # Otherwise, allow paginating through all results +    # +    all_required_elements = children +    all_required_elements |= ancestors_for_projects if params[:filter] +    total_count = [all_required_elements.size, paginator.total_count].max + +    Kaminari.paginate_array(all_required_elements, total_count: total_count) +  end + +  def has_children? +    projects.any? || subgroups.any? +  end + +  private + +  def children +    @children ||= paginator.paginate(params[:page]) +  end + +  def paginator +    @paginator ||= Gitlab::MultiCollectionPaginator.new(subgroups, projects, +                                                        per_page: params[:per_page]) +  end + +  def direct_child_groups +    GroupsFinder.new(current_user, +                     parent: parent_group, +                     all_available: true).execute +  end + +  def all_visible_descendant_groups +    groups_table = Group.arel_table +    visible_to_user = groups_table[:visibility_level] +                      .in(Gitlab::VisibilityLevel.levels_for_user(current_user)) +    if current_user +      authorized_groups = GroupsFinder.new(current_user, +                                           all_available: false) +                            .execute.as('authorized') +      authorized_to_user = groups_table.project(1).from(authorized_groups) +                             .where(authorized_groups[:id].eq(groups_table[:id])) +                             .exists +      visible_to_user = visible_to_user.or(authorized_to_user) +    end + +    hierarchy_for_parent +      .descendants +      .where(visible_to_user) +  end + +  def subgroups_matching_filter +    all_visible_descendant_groups +      .search(params[:filter]) +  end + +  # When filtering we want all to preload all the ancestors upto the specified +  # parent group. +  # +  # - root +  #   - subgroup +  #     - nested-group +  #       - project +  # +  # So when searching 'project', on the 'subgroup' page we want to preload +  # 'nested-group' but not 'subgroup' or 'root' +  def ancestors_for_groups(base_for_ancestors) +    Gitlab::GroupHierarchy.new(base_for_ancestors) +      .base_and_ancestors(upto: parent_group.id) +  end + +  def ancestors_for_projects +    projects_to_load_ancestors_of = projects.where.not(namespace: parent_group) +    groups_to_load_ancestors_of = Group.where(id: projects_to_load_ancestors_of.select(:namespace_id)) +    ancestors_for_groups(groups_to_load_ancestors_of) +      .with_selects_for_list(archived: params[:archived]) +  end + +  def subgroups +    return Group.none unless Group.supports_nested_groups? + +    # When filtering subgroups, we want to find all matches withing the tree of +    # descendants to show to the user +    groups = if params[:filter] +               ancestors_for_groups(subgroups_matching_filter) +             else +               direct_child_groups +             end +    groups.with_selects_for_list(archived: params[:archived]).order_by(sort) +  end + +  def direct_child_projects +    GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params) +      .execute +  end + +  # Finds all projects nested under `parent_group` or any of its descendant +  # groups +  def projects_matching_filter +    projects_nested_in_group = Project.where(namespace_id: hierarchy_for_parent.base_and_descendants.select(:id)) +    params_with_search = params.merge(search: params[:filter]) + +    ProjectsFinder.new(params: params_with_search, +                       current_user: current_user, +                       project_ids_relation: projects_nested_in_group).execute +  end + +  def projects +    projects = if params[:filter] +                 projects_matching_filter +               else +                 direct_child_projects +               end +    projects.with_route.order_by(sort) +  end + +  def sort +    params.fetch(:sort, 'id_asc') +  end + +  def hierarchy_for_parent +    @hierarchy ||= Gitlab::GroupHierarchy.new(Group.where(id: parent_group.id)) +  end +end diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index f2d3b90b8e2..6e8733bb49c 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -34,7 +34,6 @@ class GroupProjectsFinder < ProjectsFinder                 else                   collection_without_user                 end -      union(projects)    end diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index 0c4c4b10fb6..0282b378d88 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -15,6 +15,8 @@  # Anonymous users will never return any `owned` groups. They will return all  # public groups instead, even if `all_available` is set to false.  class GroupsFinder < UnionFinder +  include CustomAttributesFilter +    def initialize(current_user = nil, params = {})      @current_user = current_user      @params = params @@ -22,8 +24,12 @@ class GroupsFinder < UnionFinder    def execute      items = all_groups.map do |item| -      by_parent(item) +      item = by_parent(item) +      item = by_custom_attributes(item) + +      item      end +      find_union(items, Group).with_route.order_id_desc    end diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index eac6095d8dc..005612ededc 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -18,6 +18,8 @@  #     non_archived: boolean  #  class ProjectsFinder < UnionFinder +  include CustomAttributesFilter +    attr_accessor :params    attr_reader :current_user, :project_ids_relation @@ -44,6 +46,7 @@ class ProjectsFinder < UnionFinder      collection = by_tags(collection)      collection = by_search(collection)      collection = by_archived(collection) +    collection = by_custom_attributes(collection)      sort(collection)    end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8d02d5de5c3..4754a67450f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -309,4 +309,8 @@ module ApplicationHelper    def show_new_repo?      cookies["new_repo"] == "true" && body_data_page != 'projects:show'    end + +  def locale_path +    asset_path("locale/#{Gitlab::I18n.locale}/app.js") +  end  end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 7bd34df5c95..cd1ecaadb85 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -108,6 +108,43 @@ module ApplicationSettingsHelper      options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues)    end +  def circuitbreaker_failure_count_help_text +    health_link = link_to(s_('AdminHealthPageLink|health page'), admin_health_check_path) +    api_link = link_to(s_('CircuitBreakerApiLink|circuitbreaker api'), help_page_path("api/repository_storage_health")) +    message = _("The number of failures of after which GitLab will completely "\ +                "prevent access to the storage. The number of failures can be "\ +                "reset in the admin interface: %{link_to_health_page} or using "\ +                "the %{api_documentation_link}.") +    message = message % { link_to_health_page: health_link, api_documentation_link: api_link } + +    message.html_safe +  end + +  def circuitbreaker_access_retries_help_text +    _('The number of attempts GitLab will make to access a storage.') +  end + +  def circuitbreaker_backoff_threshold_help_text +    _("The number of failures after which GitLab will start temporarily "\ +      "disabling access to a storage shard on a host") +  end + +  def circuitbreaker_failure_wait_time_help_text +    _("When access to a storage fails. GitLab will prevent access to the "\ +      "storage for the time specified here. This allows the filesystem to "\ +      "recover. Repositories on failing shards are temporarly unavailable") +  end + +  def circuitbreaker_failure_reset_time_help_text +    _("The time in seconds GitLab will keep failure information. When no "\ +      "failures occur during this time, information about the mount is reset.") +  end + +  def circuitbreaker_storage_timeout_help_text +    _("The time in seconds GitLab will try to access storage. After this time a "\ +      "timeout error will be raised.") +  end +    def visible_attributes      [        :admin_notification_email, @@ -116,6 +153,12 @@ module ApplicationSettingsHelper        :akismet_api_key,        :akismet_enabled,        :auto_devops_enabled, +      :circuitbreaker_access_retries, +      :circuitbreaker_backoff_threshold, +      :circuitbreaker_failure_count_threshold, +      :circuitbreaker_failure_reset_time, +      :circuitbreaker_failure_wait_time, +      :circuitbreaker_storage_timeout,        :clientside_sentry_dsn,        :clientside_sentry_enabled,        :container_registry_token_expire_delay, diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 7112c6ee470..c4a621160af 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -20,17 +20,6 @@ module BoardsHelper      project_issues_path(@project)    end -  def current_board_json -    board = @board || @boards.first - -    board.to_json( -      only: [:id, :name, :milestone_id], -      include: { -        milestone: { only: [:title] } -      } -    ) -  end -    def board_base_url      project_boards_path(@project)    end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8022547a6ad..4dd573c61f1 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -63,34 +63,34 @@ module CiStatusHelper    def ci_icon_for_status(status)      if detailed_status?(status) -      return custom_icon(status.icon) +      return sprite_icon(status.icon)      end      icon_name =        case status        when 'success' -        'icon_status_success' +        'status_success'        when 'success_with_warnings' -        'icon_status_warning' +        'status_warning'        when 'failed' -        'icon_status_failed' +        'status_failed'        when 'pending' -        'icon_status_pending' +        'status_pending'        when 'running' -        'icon_status_running' +        'status_running'        when 'play' -        'icon_play' +        'play'        when 'created' -        'icon_status_created' +        'status_created'        when 'skipped' -        'icon_status_skipped' +        'status_skipped'        when 'manual' -        'icon_status_manual' +        'status_manual'        else -        'icon_status_canceled' +        'status_canceled'        end -    custom_icon(icon_name) +    sprite_icon(icon_name, size: 16)    end    def pipeline_status_cache_key(pipeline_status) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index ef22cafc2e2..f9a666fa1e6 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -60,23 +60,33 @@ module CommitsHelper      branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop    end +  # Returns a link formatted as a commit branch link +  def commit_branch_link(url, text) +    link_to(url, class: 'label label-gray ref-name branch-link') do +      icon('code-fork') + " #{text}" +    end +  end +    # Returns the sorted alphabetically links to branches, separated by a comma    def commit_branches_links(project, branches)      branches.sort.map do |branch| -      link_to(project_ref_path(project, branch), class: "label label-gray ref-name") do -        icon('code-fork') + " #{branch}" -      end -    end.join(" ").html_safe +      commit_branch_link(project_ref_path(project, branch), branch) +    end.join(' ').html_safe +  end + +  # Returns a link formatted as a commit tag link +  def commit_tag_link(url, text) +    link_to(url, class: 'label label-gray ref-name') do +      icon('tag') + " #{text}" +    end    end    # Returns the sorted links to tags, separated by a comma    def commit_tags_links(project, tags)      sorted = VersionSorter.rsort(tags)      sorted.map do |tag| -      link_to(project_ref_path(project, tag), class: "label label-gray ref-name") do -        icon('tag') + " #{tag}" -      end -    end.join(" ").html_safe +      commit_tag_link(project_ref_path(project, tag), tag) +    end.join(' ').html_safe    end    def link_to_browse_code(project, commit) diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index 2c28dd81c87..8bf96c0905f 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -4,8 +4,8 @@ module CompareHelper        to.present? &&        from != to &&        can?(current_user, :create_merge_request, project) && -      project.repository.branch_names.include?(from) && -      project.repository.branch_names.include?(to) +      project.repository.branch_exists?(from) && +      project.repository.branch_exists?(to)    end    def create_mr_path(from = params[:from], to = params[:to], project = @project) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index fd88e0d794a..079b3cd3aa0 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -172,16 +172,6 @@ module EventsHelper      end    end -  def event_note(text, options = {}) -    text = first_line_in_markdown(text, 150, options) - -    sanitize( -      text, -      tags: %w(a img gl-emoji b pre code p span), -      attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version'] -    ) -  end -    def event_commit_title(message)      message ||= ''      (message.split("\n").first || "").truncate(70) diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index d4a91e533c1..a77aa0ad2cc 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -71,11 +71,13 @@ module GitlabRoutingHelper      project_commit_url(entity.project, entity.sha, *args)    end -  def preview_markdown_path(project, *args) +  def preview_markdown_path(parent, *args) +    return group_preview_markdown_path(parent) if parent.is_a?(Group) +      if @snippet.is_a?(PersonalSnippet)        preview_markdown_snippets_path      else -      preview_markdown_project_path(project, *args) +      preview_markdown_project_path(parent, *args)      end    end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 82bceddf1f0..676c1d1988b 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -7,7 +7,12 @@ module GroupsHelper      can?(current_user, :change_share_with_group_lock, group)    end -  def group_icon(group) +  def group_icon(group, options = {}) +    img_path = group_icon_url(group, options) +    image_tag img_path, options +  end + +  def group_icon_url(group, options = {})      if group.is_a?(String)        group = Group.find_by_full_path(group)      end @@ -89,7 +94,7 @@ module GroupsHelper      link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do        output =          if (group.try(:avatar_url) || show_avatar) && !Rails.env.test? -          image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15) +          group_icon(group, class: "avatar-tile", width: 15, height: 15)          else            ""          end diff --git a/app/helpers/instance_configuration_helper.rb b/app/helpers/instance_configuration_helper.rb new file mode 100644 index 00000000000..cee319f20bc --- /dev/null +++ b/app/helpers/instance_configuration_helper.rb @@ -0,0 +1,18 @@ +module InstanceConfigurationHelper +  def instance_configuration_cell_html(value, &block) +    return '-' unless value.to_s.presence + +    block_given? ? yield(value) : value +  end + +  def instance_configuration_host(host) +    @instance_configuration_host ||= instance_configuration_cell_html(host).capitalize +  end + +  # Value must be in bytes +  def instance_configuration_human_size_cell(value) +    instance_configuration_cell_html(value) do |v| +      number_to_human_size(v, strip_insignificant_zeros: true, significant: false) +    end +  end +end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 7713fb0b9f8..a9840d19178 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -33,15 +33,17 @@ module IssuablesHelper    end    def serialize_issuable(issuable) -    case issuable -    when Issue -      IssueSerializer.new(current_user: current_user, project: issuable.project).represent(issuable).to_json -    when MergeRequest -      MergeRequestSerializer -        .new(current_user: current_user, project: issuable.project) -        .represent(issuable) -        .to_json -    end +    serializer_klass = case issuable +                       when Issue +                         IssueSerializer +                       when MergeRequest +                         MergeRequestSerializer +                       end + +    serializer_klass +      .new(current_user: current_user, project: issuable.project) +      .represent(issuable) +      .to_json    end    def template_dropdown_tag(issuable, &block) @@ -209,15 +211,13 @@ module IssuablesHelper    def issuable_initial_data(issuable)      data = { -      endpoint: project_issue_path(@project, issuable), -      canUpdate: can?(current_user, :update_issue, issuable), -      canDestroy: can?(current_user, :destroy_issue, issuable), +      endpoint: issuable_path(issuable), +      canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable), +      canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),        issuableRef: issuable.to_reference, -      markdownPreviewPath: preview_markdown_path(@project), +      markdownPreviewPath: preview_markdown_path(parent),        markdownDocsPath: help_page_path('user/markdown'),        issuableTemplates: issuable_templates(issuable), -      projectPath: ref_project.path, -      projectNamespace: ref_project.namespace.full_path,        initialTitleHtml: markdown_field(issuable, :title),        initialTitleText: issuable.title,        initialDescriptionHtml: markdown_field(issuable, :description), @@ -225,6 +225,12 @@ module IssuablesHelper        initialTaskStatus: issuable.task_status      } +    if parent.is_a?(Group) +      data[:groupPath] = parent.path +    else +      data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path) +    end +      data.merge!(updated_at_by(issuable))      data.to_json @@ -243,8 +249,6 @@ module IssuablesHelper    end    def issuables_count_for_state(issuable_type, state) -    finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend -      Gitlab::IssuablesCountForState.new(finder)[state]    end @@ -261,12 +265,7 @@ module IssuablesHelper    end    def issuable_path(issuable, *options) -    case issuable -    when Issue -      issue_path(issuable, *options) -    when MergeRequest -      merge_request_path(issuable, *options) -    end +    polymorphic_path(issuable, *options)    end    def issuable_url(issuable, *options) @@ -314,20 +313,12 @@ module IssuablesHelper      @issuable_templates ||=        case issuable        when Issue -        issue_template_names +        ref_project.repository.issue_template_names        when MergeRequest -        merge_request_template_names +        ref_project.repository.merge_request_template_names        end    end -  def merge_request_template_names -    @merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project) -  end - -  def issue_template_names -    @issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project) -  end -    def selected_template(issuable)      params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }    end @@ -365,7 +356,8 @@ module IssuablesHelper    def issuable_sidebar_options(issuable, can_edit_issuable)      { -      endpoint: "#{issuable_json_path(issuable)}?basic=true", +      endpoint: "#{issuable_json_path(issuable)}?serializer=sidebar", +      toggleSubscriptionEndpoint: toggle_subscription_path(issuable),        moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),        projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),        editable: can_edit_issuable, @@ -374,4 +366,8 @@ module IssuablesHelper        fullPath: @project.full_path      }    end + +  def parent +    @project || @group +  end  end diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb index 2c5619ac41b..603b9438e35 100644 --- a/app/helpers/lazy_image_tag_helper.rb +++ b/app/helpers/lazy_image_tag_helper.rb @@ -10,6 +10,7 @@ module LazyImageTagHelper      unless options.delete(:lazy) == false        options[:data] ||= {}        options[:data][:src] = path_to_image(source) +        options[:class] ||= ""        options[:class] << " lazy" diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 46bced00c72..2c85d7d7720 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -69,10 +69,16 @@ module MarkupHelper    # as Markdown.  HTML tags in the parsed output are not counted toward the    # +max_chars+ limit.  If the length limit falls within a tag's contents, then    # the tag contents are truncated without removing the closing tag. -  def first_line_in_markdown(text, max_chars = nil, options = {}) -    md = markdown(text, options).strip +  def first_line_in_markdown(object, attribute, max_chars = nil, options = {}) +    md = markdown_field(object, attribute, options) -    truncate_visible(md, max_chars || md.length) if md.present? +    text = truncate_visible(md, max_chars || md.length) if md.present? + +    sanitize( +      text, +      tags: %w(a img gl-emoji b pre code p span), +      attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version'] +    )    end    def markdown(text, context = {}) @@ -83,15 +89,17 @@ module MarkupHelper      prepare_for_rendering(html, context)    end -  def markdown_field(object, field) +  def markdown_field(object, field, context = {})      object = object.for_display if object.respond_to?(:for_display)      redacted_field_html = object.try(:"redacted_#{field}_html")      return '' unless object.present?      return redacted_field_html if redacted_field_html -    html = Banzai.render_field(object, field) -    prepare_for_rendering(html, object.banzai_render_context(field)) +    html = Banzai.render_field(object, field, context) +    context.reverse_merge!(object.banzai_render_context(field)) if object.respond_to?(:banzai_render_context) + +    prepare_for_rendering(html, context)    end    def markup(file_name, text, context = {}) @@ -218,7 +226,7 @@ module MarkupHelper        data: data,        title: options[:title],        aria: { label: options[:title] } do -      icon(options[:icon]) +      sprite_icon(options[:icon])      end    end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index a23a43c9f43..8ada746b244 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,7 +1,7 @@  module NavHelper    def page_with_sidebar_class      class_name = page_gutter_class -    class_name << 'page-with-new-sidebar' if defined?(@left_sidebar) && @left_sidebar +    class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar      class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar      class_name @@ -19,11 +19,7 @@ module NavHelper        end      elsif current_path?('jobs#show')        %w[page-gutter build-sidebar right-sidebar-expanded] -    elsif current_path?('wikis#show') || -        current_path?('wikis#edit') || -        current_path?('wikis#update') || -        current_path?('wikis#history') || -        current_path?('wikis#git_access') +    elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access')        %w[page-gutter wiki-sidebar right-sidebar-expanded]      else        [] diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 0d7347ed30d..8e822ed0ea2 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -36,7 +36,8 @@ module PreferencesHelper    def project_view_choices      [        ['Files and Readme (default)', :files], -      ['Activity', :activity] +      ['Activity', :activity], +      ['Readme', :readme]      ]    end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 20e050195ea..f48d47953e4 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -15,7 +15,7 @@ module ProjectsHelper    end    def link_to_member_avatar(author, opts = {}) -    default_opts = { size: 16 } +    default_opts = { size: 16, lazy_load: false }      opts = default_opts.merge(opts)      classes = %W[avatar avatar-inline s#{opts[:size]}] @@ -27,8 +27,26 @@ module ProjectsHelper      image_tag(src, width: opts[:size], class: classes, alt: '', "data-src" => avatar)    end +  def author_content_tag(author, opts = {}) +    default_opts = { author_class: 'author', tooltip: false, by_username: false } +    opts = default_opts.merge(opts) + +    has_tooltip = !opts[:by_username] && opts[:tooltip] + +    username = opts[:by_username] ? author.to_reference : author.name +    name_tag_options = { class: [opts[:author_class]] } + +    if has_tooltip +      name_tag_options[:title] = author.to_reference +      name_tag_options[:data] = { placement: 'top' } +      name_tag_options[:class] << 'has-tooltip' +    end + +    content_tag(:span, sanitize(username), name_tag_options) +  end +    def link_to_member(project, author, opts = {}, &block) -    default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name", tooltip: false, lazy_load: false } +    default_opts = { avatar: true, name: true, title: ":name" }      opts = default_opts.merge(opts)      return "(deleted)" unless author @@ -39,12 +57,7 @@ module ProjectsHelper      author_html << link_to_member_avatar(author, opts) if opts[:avatar]      # Build name span tag -    if opts[:by_username] -      author_html << content_tag(:span, sanitize("@#{author.username}"), class: opts[:author_class]) if opts[:name] -    else -      tooltip_data = { placement: 'top' } -      author_html << content_tag(:span, sanitize(author.name), class: [opts[:author_class], ('has-tooltip' if opts[:tooltip])], title: (author.to_reference if opts[:tooltip]), data: (tooltip_data if opts[:tooltip])) if opts[:name] -    end +    author_html << author_content_tag(author, opts) if opts[:name]      author_html << capture(&block) if block @@ -97,7 +110,15 @@ module ProjectsHelper    def remove_fork_project_message(project)      _("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") % -      { forked_from_project: @project.forked_from_project.name_with_namespace } +      { forked_from_project: fork_source_name(project) } +  end + +  def fork_source_name(project) +    if @project.fork_source +      @project.fork_source.full_name +    else +      @project.fork_network&.deleted_root_project_name +    end    end    def project_nav_tabs @@ -127,8 +148,8 @@ module ProjectsHelper    def can_change_visibility_level?(project, current_user)      return false unless can?(current_user, :change_visibility_level, project) -    if project.forked? -      project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE +    if project.fork_source +      project.fork_source.visibility_level > Gitlab::VisibilityLevel::PRIVATE      else        true      end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 1b542ed2a96..b05eb93b465 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -42,6 +42,17 @@ module SortingHelper      options    end +  def groups_sort_options_hash +    options = { +      sort_value_recently_created => sort_title_recently_created, +      sort_value_oldest_created => sort_title_oldest_created, +      sort_value_recently_updated => sort_title_recently_updated, +      sort_value_oldest_updated => sort_title_oldest_updated +    } + +    options +  end +    def member_sort_options_hash      {        sort_value_access_level_asc  => sort_title_access_level_asc, diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb index 544c9efb845..4d2180f7eee 100644 --- a/app/helpers/storage_health_helper.rb +++ b/app/helpers/storage_health_helper.rb @@ -16,17 +16,16 @@ module StorageHealthHelper    def message_for_circuit_breaker(circuit_breaker)      maximum_failures = circuit_breaker.failure_count_threshold      current_failures = circuit_breaker.failure_count -    permanently_broken = circuit_breaker.circuit_broken? && current_failures >= maximum_failures      translation_params = { number_of_failures: current_failures,                             maximum_failures: maximum_failures,                             number_of_seconds: circuit_breaker.failure_wait_time } -    if permanently_broken +    if circuit_breaker.circuit_broken?        s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\           "retry automatically. Reset storage information when the problem is "\           "resolved.") % translation_params -    elsif circuit_breaker.circuit_broken? +    elsif circuit_breaker.backing_off?        _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\          "block access for %{number_of_seconds} seconds.") % translation_params      else diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index c0cc60d5ebf..5e16badabec 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -33,6 +33,8 @@ class ApplicationSetting < ActiveRecord::Base    attr_accessor :domain_whitelist_raw, :domain_blacklist_raw +  default_value_for :id, 1 +    validates :uuid, presence: true    validates :session_expire_delay, @@ -151,6 +153,25 @@ class ApplicationSetting < ActiveRecord::Base              presence: true,              numericality: { greater_than_or_equal_to: 0 } +  validates :circuitbreaker_backoff_threshold, +            :circuitbreaker_failure_count_threshold, +            :circuitbreaker_failure_wait_time, +            :circuitbreaker_failure_reset_time, +            :circuitbreaker_storage_timeout, +            presence: true, +            numericality: { only_integer: true, greater_than_or_equal_to: 0 } + +  validates :circuitbreaker_access_retries, +            presence: true, +            numericality: { only_integer: true, greater_than_or_equal_to: 1 } + +  validates_each :circuitbreaker_backoff_threshold do |record, attr, value| +    if value.to_i >= record.circuitbreaker_failure_count_threshold +      record.errors.add(attr, _("The circuitbreaker backoff threshold should be "\ +                                "lower than the failure count threshold")) +    end +  end +    SUPPORTED_KEY_TYPES.each do |type|      validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }    end @@ -194,7 +215,10 @@ class ApplicationSetting < ActiveRecord::Base      ensure_cache_setup      Rails.cache.fetch(CACHE_KEY) do -      ApplicationSetting.last +      ApplicationSetting.last.tap do |settings| +        # do not cache nils +        raise 'missing settings' unless settings +      end      end    rescue      # Fall back to an uncached value if there are any problems (e.g. redis down) @@ -396,7 +420,7 @@ class ApplicationSetting < ActiveRecord::Base    #   the enabling/disabling is `performance_bar_allowed_group_id`    # - If `enable` is false, we set `performance_bar_allowed_group_id` to `nil`    def performance_bar_enabled=(enable) -    return if enable +    return if Gitlab::Utils.to_boolean(enable)      self.performance_bar_allowed_group_id = nil    end diff --git a/app/models/blob.rb b/app/models/blob.rb index 954d4e4d779..ad0bc2e2ead 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -156,7 +156,9 @@ class Blob < SimpleDelegator    end    def file_type -    Gitlab::FileDetector.type_of(path) +    name = File.basename(path) + +    Gitlab::FileDetector.type_of(path) || Gitlab::FileDetector.type_of(name)    end    def video? diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb index 8b66531ec7b..ec56cc53aea 100644 --- a/app/models/ci/artifact_blob.rb +++ b/app/models/ci/artifact_blob.rb @@ -2,7 +2,7 @@ module Ci    class ArtifactBlob      include BlobLike -    EXTENTIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json].freeze +    EXTENSIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json].freeze      attr_reader :entry @@ -36,17 +36,22 @@ module Ci      def external_url(project, job)        return unless external_link?(job) -      components = project.full_path_components -      components << "-/jobs/#{job.id}/artifacts/file/#{path}" -      artifact_path = components[1..-1].join('/') +      full_path_parts = project.full_path_components +      top_level_group = full_path_parts.shift -      "#{pages_config.protocol}://#{components[0]}.#{pages_config.host}/#{artifact_path}" +      artifact_path = [ +        '-', *full_path_parts, '-', +        'jobs', job.id, +        'artifacts', path +      ].join('/') + +      "#{pages_config.protocol}://#{top_level_group}.#{pages_config.host}/#{artifact_path}"      end      def external_link?(job)        pages_config.enabled &&          pages_config.artifacts_server && -        EXTENTIONS_SERVED_BY_PAGES.include?(File.extname(name)) && +        EXTENSIONS_SERVED_BY_PAGES.include?(File.extname(name)) &&          job.project.public?      end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index cf3ce3c9e54..ca65e81f27a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -249,9 +249,7 @@ module Ci      end      def commit -      @commit ||= project.commit(sha) -    rescue -      nil +      @commit ||= project.commit_by(oid: sha)      end      def branch? diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb new file mode 100644 index 00000000000..c7949d11ef8 --- /dev/null +++ b/app/models/clusters/applications/helm.rb @@ -0,0 +1,35 @@ +module Clusters +  module Applications +    class Helm < ActiveRecord::Base +      self.table_name = 'clusters_applications_helm' + +      include ::Clusters::Concerns::ApplicationStatus + +      belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id + +      default_value_for :version, Gitlab::Kubernetes::Helm::HELM_VERSION + +      validates :cluster, presence: true + +      after_initialize :set_initial_status + +      def self.application_name +        self.to_s.demodulize.underscore +      end + +      def set_initial_status +        return unless not_installable? + +        self.status = 'installable' if cluster&.platform_kubernetes_active? +      end + +      def name +        self.class.application_name +      end + +      def install_command +        Gitlab::Kubernetes::Helm::InstallCommand.new(name, true) +      end +    end +  end +end diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb new file mode 100644 index 00000000000..44bd979741e --- /dev/null +++ b/app/models/clusters/applications/ingress.rb @@ -0,0 +1,44 @@ +module Clusters +  module Applications +    class Ingress < ActiveRecord::Base +      self.table_name = 'clusters_applications_ingress' + +      include ::Clusters::Concerns::ApplicationStatus + +      belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id + +      validates :cluster, presence: true + +      default_value_for :ingress_type, :nginx +      default_value_for :version, :nginx + +      after_initialize :set_initial_status + +      enum ingress_type: { +        nginx: 1 +      } + +      def self.application_name +        self.to_s.demodulize.underscore +      end + +      def set_initial_status +        return unless not_installable? + +        self.status = 'installable' if cluster&.application_helm_installed? +      end + +      def name +        self.class.application_name +      end + +      def chart +        'stable/nginx-ingress' +      end + +      def install_command +        Gitlab::Kubernetes::Helm::InstallCommand.new(name, false, chart) +      end +    end +  end +end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb new file mode 100644 index 00000000000..185d9473aab --- /dev/null +++ b/app/models/clusters/cluster.rb @@ -0,0 +1,102 @@ +module Clusters +  class Cluster < ActiveRecord::Base +    include Presentable + +    self.table_name = 'clusters' + +    APPLICATIONS = { +      Applications::Helm.application_name => Applications::Helm, +      Applications::Ingress.application_name => Applications::Ingress +    }.freeze + +    belongs_to :user + +    has_many :cluster_projects, class_name: 'Clusters::Project' +    has_many :projects, through: :cluster_projects, class_name: '::Project' + +    # we force autosave to happen when we save `Cluster` model +    has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true + +    # We have to ":destroy" it today to ensure that we clean also the Kubernetes Integration +    has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + +    has_one :application_helm, class_name: 'Clusters::Applications::Helm' +    has_one :application_ingress, class_name: 'Clusters::Applications::Ingress' + +    accepts_nested_attributes_for :provider_gcp, update_only: true +    accepts_nested_attributes_for :platform_kubernetes, update_only: true + +    validates :name, cluster_name: true +    validate :restrict_modification, on: :update + +    # TODO: Move back this into Clusters::Platforms::Kubernetes in 10.3 +    # We need callback here because `enabled` belongs to Clusters::Cluster +    # Callbacks in Clusters::Platforms::Kubernetes will not be called after update +    after_save :update_kubernetes_integration! + +    delegate :status, to: :provider, allow_nil: true +    delegate :status_reason, to: :provider, allow_nil: true +    delegate :on_creation?, to: :provider, allow_nil: true +    delegate :update_kubernetes_integration!, to: :platform, allow_nil: true + +    delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true +    delegate :installed?, to: :application_helm, prefix: true, allow_nil: true + +    enum platform_type: { +      kubernetes: 1 +    } + +    enum provider_type: { +      user: 0, +      gcp: 1 +    } + +    scope :enabled, -> { where(enabled: true) } +    scope :disabled, -> { where(enabled: false) } + +    def status_name +      if provider +        provider.status_name +      else +        :created +      end +    end + +    def applications +      [ +        application_helm || build_application_helm, +        application_ingress || build_application_ingress +      ] +    end + +    def provider +      return provider_gcp if gcp? +    end + +    def platform +      return platform_kubernetes if kubernetes? +    end + +    def first_project +      return @first_project if defined?(@first_project) + +      @first_project = projects.first +    end +    alias_method :project, :first_project + +    def kubeclient +      platform_kubernetes.kubeclient if kubernetes? +    end + +    private + +    def restrict_modification +      if provider&.on_creation? +        errors.add(:base, "cannot modify during creation") +        return false +      end + +      true +    end +  end +end diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb new file mode 100644 index 00000000000..7b7c8eac773 --- /dev/null +++ b/app/models/clusters/concerns/application_status.rb @@ -0,0 +1,43 @@ +module Clusters +  module Concerns +    module ApplicationStatus +      extend ActiveSupport::Concern + +      included do +        state_machine :status, initial: :not_installable do +          state :not_installable, value: -2 +          state :errored, value: -1 +          state :installable, value: 0 +          state :scheduled, value: 1 +          state :installing, value: 2 +          state :installed, value: 3 + +          event :make_scheduled do +            transition [:installable, :errored] => :scheduled +          end + +          event :make_installing do +            transition [:scheduled] => :installing +          end + +          event :make_installed do +            transition [:installing] => :installed +          end + +          event :make_errored do +            transition any => :errored +          end + +          before_transition any => [:scheduled] do |app_status, _| +            app_status.status_reason = nil +          end + +          before_transition any => [:errored] do |app_status, transition| +            status_reason = transition.args.first +            app_status.status_reason = status_reason if status_reason +          end +        end +      end +    end +  end +end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb new file mode 100644 index 00000000000..6dc1ee810d3 --- /dev/null +++ b/app/models/clusters/platforms/kubernetes.rb @@ -0,0 +1,109 @@ +module Clusters +  module Platforms +    class Kubernetes < ActiveRecord::Base +      self.table_name = 'cluster_platforms_kubernetes' + +      belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster' + +      attr_encrypted :password, +        mode: :per_attribute_iv, +        key: Gitlab::Application.secrets.db_key_base, +        algorithm: 'aes-256-cbc' + +      attr_encrypted :token, +        mode: :per_attribute_iv, +        key: Gitlab::Application.secrets.db_key_base, +        algorithm: 'aes-256-cbc' + +      before_validation :enforce_namespace_to_lower_case + +      validates :namespace, +        allow_blank: true, +        length: 1..63, +        format: { +          with: Gitlab::Regex.kubernetes_namespace_regex, +          message: Gitlab::Regex.kubernetes_namespace_regex_message +        } + +      # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned) +      validates :api_url, url: true, presence: true +      validates :token, presence: true + +      # TODO: Glue code till we migrate Kubernetes Integration into Platforms::Kubernetes +      after_destroy :destroy_kubernetes_integration! + +      alias_attribute :ca_pem, :ca_cert + +      delegate :project, to: :cluster, allow_nil: true +      delegate :enabled?, to: :cluster, allow_nil: true + +      class << self +        def namespace_for_project(project) +          "#{project.path}-#{project.id}" +        end +      end + +      def actual_namespace +        if namespace.present? +          namespace +        else +          default_namespace +        end +      end + +      def default_namespace +        self.class.namespace_for_project(project) if project +      end + +      def kubeclient +        @kubeclient ||= kubernetes_service.kubeclient if manages_kubernetes_service? +      end + +      def update_kubernetes_integration! +        raise 'Kubernetes service already configured' unless manages_kubernetes_service? + +        # This is neccesary, otheriwse enabled? returns true even though cluster updated with enabled: false +        cluster.reload + +        ensure_kubernetes_service&.update!( +          active: enabled?, +          api_url: api_url, +          namespace: namespace, +          token: token, +          ca_pem: ca_cert +        ) +      end + +      def active? +        manages_kubernetes_service? +      end + +      private + +      def enforce_namespace_to_lower_case +        self.namespace = self.namespace&.downcase +      end + +      # TODO: glue code till we migrate Kubernetes Service into Platforms::Kubernetes class +      def manages_kubernetes_service? +        return true unless kubernetes_service&.active? + +        kubernetes_service.api_url == api_url +      end + +      def destroy_kubernetes_integration! +        return unless manages_kubernetes_service? + +        kubernetes_service&.destroy! +      end + +      def kubernetes_service +        @kubernetes_service ||= project&.kubernetes_service +      end + +      def ensure_kubernetes_service +        @kubernetes_service ||= kubernetes_service || project&.build_kubernetes_service +      end +    end +  end +end diff --git a/app/models/clusters/project.rb b/app/models/clusters/project.rb new file mode 100644 index 00000000000..eeb734b20b8 --- /dev/null +++ b/app/models/clusters/project.rb @@ -0,0 +1,8 @@ +module Clusters +  class Project < ActiveRecord::Base +    self.table_name = 'cluster_projects' + +    belongs_to :cluster, class_name: 'Clusters::Cluster' +    belongs_to :project, class_name: '::Project' +  end +end diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb new file mode 100644 index 00000000000..ee2e43ee9dd --- /dev/null +++ b/app/models/clusters/providers/gcp.rb @@ -0,0 +1,79 @@ +module Clusters +  module Providers +    class Gcp < ActiveRecord::Base +      self.table_name = 'cluster_providers_gcp' + +      belongs_to :cluster, inverse_of: :provider_gcp, class_name: 'Clusters::Cluster' + +      default_value_for :zone, 'us-central1-a' +      default_value_for :num_nodes, 3 +      default_value_for :machine_type, 'n1-standard-2' + +      attr_encrypted :access_token, +        mode: :per_attribute_iv, +        key: Gitlab::Application.secrets.db_key_base, +        algorithm: 'aes-256-cbc' + +      validates :gcp_project_id, +        length: 1..63, +        format: { +          with: Gitlab::Regex.kubernetes_namespace_regex, +          message: Gitlab::Regex.kubernetes_namespace_regex_message +        } + +      validates :zone, presence: true + +      validates :num_nodes, +        presence: true, +        numericality: { +          only_integer: true, +          greater_than: 0 +        } + +      state_machine :status, initial: :scheduled do +        state :scheduled, value: 1 +        state :creating, value: 2 +        state :created, value: 3 +        state :errored, value: 4 + +        event :make_creating do +          transition any - [:creating] => :creating +        end + +        event :make_created do +          transition any - [:created] => :created +        end + +        event :make_errored do +          transition any - [:errored] => :errored +        end + +        before_transition any => [:errored, :created] do |provider| +          provider.access_token = nil +          provider.operation_id = nil +        end + +        before_transition any => [:creating] do |provider, transition| +          operation_id = transition.args.first +          raise ArgumentError.new('operation_id is required') unless operation_id.present? +          provider.operation_id = operation_id +        end + +        before_transition any => [:errored] do |provider, transition| +          status_reason = transition.args.first +          provider.status_reason = status_reason if status_reason +        end +      end + +      def on_creation? +        scheduled? || creating? +      end + +      def api_client +        return unless access_token + +        @api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil) +      end +    end +  end +end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index f3888528940..6b07dbdf3ea 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -14,7 +14,6 @@ class CommitStatus < ActiveRecord::Base    delegate :sha, :short_sha, to: :pipeline    validates :pipeline, presence: true, unless: :importing? -    validates :name, presence: true, unless: :importing?    alias_attribute :author, :user @@ -46,6 +45,17 @@ class CommitStatus < ActiveRecord::Base      runner_system_failure: 4    } +  ## +  # We still create some CommitStatuses outside of CreatePipelineService. +  # +  # These are pages deployments and external statuses. +  # +  before_create unless: :importing? do +    Ci::EnsureStageService.new(project, user).execute(self) do |stage| +      self.run_after_commit { StageUpdateWorker.perform_async(stage.id) } +    end +  end +    state_machine :status do      event :process do        transition [:skipped, :manual] => :created diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index 8fbfed11bdf..2ec70203710 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -11,7 +11,7 @@ module Avatarable      # If asset_host is set then it is expected that assets are handled by a standalone host.      # That means we do not want to get GitLab's relative_url_root option anymore. -    host = asset_host.present? ? asset_host : gitlab_host +    host = (asset_host.present? && (!respond_to?(:public?) || public?)) ? asset_host : gitlab_host      [host, avatar.url].join    end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 9417033d1f6..98776eab424 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -49,7 +49,8 @@ module CacheMarkdownField      # Always include a project key, or Banzai complains      project = self.project if self.respond_to?(:project) -    context = cached_markdown_fields[field].merge(project: project) +    group = self.group if self.respond_to?(:group) +    context = cached_markdown_fields[field].merge(project: project, group: group)      # Banzai is less strict about authors, so don't always have an author key      context[:author] = self.author if self.respond_to?(:author) diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb new file mode 100644 index 00000000000..01957da0bf3 --- /dev/null +++ b/app/models/concerns/group_descendant.rb @@ -0,0 +1,56 @@ +module GroupDescendant +  # Returns the hierarchy of a project or group in the from of a hash upto a +  # given top. +  # +  # > project.hierarchy +  # => { parent_group => { child_group => project } } +  def hierarchy(hierarchy_top = nil, preloaded = nil) +    preloaded ||= ancestors_upto(hierarchy_top) +    expand_hierarchy_for_child(self, self, hierarchy_top, preloaded) +  end + +  # Merges all hierarchies of the given groups or projects into an array of +  # hashes. All ancestors need to be loaded into the given `descendants` to avoid +  # queries down the line. +  # +  # > GroupDescendant.merge_hierarchy([project, child_group, child_group2, parent]) +  # => { parent => [{ child_group => project}, child_group2] } +  def self.build_hierarchy(descendants, hierarchy_top = nil) +    descendants = Array.wrap(descendants).uniq +    return [] if descendants.empty? + +    unless descendants.all? { |hierarchy| hierarchy.is_a?(GroupDescendant) } +      raise ArgumentError.new('element is not a hierarchy') +    end + +    all_hierarchies = descendants.map do |descendant| +      descendant.hierarchy(hierarchy_top, descendants) +    end + +    Gitlab::Utils::MergeHash.merge(all_hierarchies) +  end + +  private + +  def expand_hierarchy_for_child(child, hierarchy, hierarchy_top, preloaded) +    parent = hierarchy_top if hierarchy_top && child.parent_id == hierarchy_top.id +    parent ||= preloaded.detect { |possible_parent| possible_parent.is_a?(Group) && possible_parent.id == child.parent_id } + +    if parent.nil? && !child.parent_id.nil? +      raise ArgumentError.new('parent was not preloaded') +    end + +    if parent.nil? && hierarchy_top.present? +      raise ArgumentError.new('specified top is not part of the tree') +    end + +    if parent && parent != hierarchy_top +      expand_hierarchy_for_child(parent, +                                 { parent => hierarchy }, +                                 hierarchy_top, +                                 preloaded) +    else +      hierarchy +    end +  end +end diff --git a/app/models/concerns/ignorable_column.rb b/app/models/concerns/ignorable_column.rb index eb9f3423e48..03793e8bcbb 100644 --- a/app/models/concerns/ignorable_column.rb +++ b/app/models/concerns/ignorable_column.rb @@ -21,8 +21,8 @@ module IgnorableColumn        @ignored_columns ||= Set.new      end -    def ignore_column(name) -      ignored_columns << name.to_s +    def ignore_column(*names) +      ignored_columns.merge(names.map(&:to_s))      end    end  end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index fc30d008dea..c008fb91a16 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -14,10 +14,11 @@ module Issuable    include StripAttribute    include Awardable    include Taskable -  include TimeTrackable    include Importable    include Editable    include AfterCommitQueue +  include Sortable +  include CreatedAtFilterable    # This object is used to gather issuable meta data for displaying    # upvotes, downvotes, notes and closing merge requests count for issues and merge requests @@ -95,8 +96,6 @@ module Issuable      strip_attributes :title -    acts_as_paranoid -      after_save :record_metrics, unless: :imported?      # We want to use optimistic lock for cases when only title or description are involved @@ -256,23 +255,22 @@ module Issuable      participants(user).include?(user)    end -  def to_hook_data(user) -    hook_data = { -      object_kind: self.class.name.underscore, -      user: user.hook_attrs, -      project: project.hook_attrs, -      object_attributes: hook_attrs, -      labels: labels.map(&:hook_attrs), -      # DEPRECATED -      repository: project.hook_attrs.slice(:name, :url, :description, :homepage) -    } -    if self.is_a?(Issue) -      hook_data[:assignees] = assignees.map(&:hook_attrs) if assignees.any? -    else -      hook_data[:assignee] = assignee.hook_attrs if assignee +  def to_hook_data(user, old_labels: [], old_assignees: []) +    changes = previous_changes + +    if old_labels != labels +      changes[:labels] = [old_labels.map(&:hook_attrs), labels.map(&:hook_attrs)] +    end + +    if old_assignees != assignees +      if self.is_a?(Issue) +        changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)] +      else +        changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs] +      end      end -    hook_data +    Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes)    end    def labels_array diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb new file mode 100644 index 00000000000..dcb3b2b5ff3 --- /dev/null +++ b/app/models/concerns/loaded_in_group_list.rb @@ -0,0 +1,72 @@ +module LoadedInGroupList +  extend ActiveSupport::Concern + +  module ClassMethods +    def with_counts(archived:) +      selects_including_counts = [ +        'namespaces.*', +        "(#{project_count_sql(archived).to_sql}) AS preloaded_project_count", +        "(#{member_count_sql.to_sql}) AS preloaded_member_count", +        "(#{subgroup_count_sql.to_sql}) AS preloaded_subgroup_count" +      ] + +      select(selects_including_counts) +    end + +    def with_selects_for_list(archived: nil) +      with_route.with_counts(archived: archived) +    end + +    private + +    def project_count_sql(archived = nil) +      projects = Project.arel_table +      namespaces = Namespace.arel_table + +      base_count = projects.project(Arel.star.count.as('preloaded_project_count')) +                     .where(projects[:namespace_id].eq(namespaces[:id])) +      if archived == 'only' +        base_count.where(projects[:archived].eq(true)) +      elsif Gitlab::Utils.to_boolean(archived) +        base_count +      else +        base_count.where(projects[:archived].not_eq(true)) +      end +    end + +    def subgroup_count_sql +      namespaces = Namespace.arel_table +      children = namespaces.alias('children') + +      namespaces.project(Arel.star.count.as('preloaded_subgroup_count')) +        .from(children) +        .where(children[:parent_id].eq(namespaces[:id])) +    end + +    def member_count_sql +      members = Member.arel_table +      namespaces = Namespace.arel_table + +      members.project(Arel.star.count.as('preloaded_member_count')) +        .where(members[:source_type].eq(Namespace.name)) +        .where(members[:source_id].eq(namespaces[:id])) +        .where(members[:requested_at].eq(nil)) +    end +  end + +  def children_count +    @children_count ||= project_count + subgroup_count +  end + +  def project_count +    @project_count ||= try(:preloaded_project_count) || projects.non_archived.count +  end + +  def subgroup_count +    @subgroup_count ||= try(:preloaded_subgroup_count) || children.count +  end + +  def member_count +    @member_count ||= try(:preloaded_member_count) || users.count +  end +end diff --git a/app/models/concerns/repository_mirroring.rb b/app/models/concerns/repository_mirroring.rb deleted file mode 100644 index f6aba91bc4c..00000000000 --- a/app/models/concerns/repository_mirroring.rb +++ /dev/null @@ -1,32 +0,0 @@ -module RepositoryMirroring -  IMPORT_HEAD_REFS = '+refs/heads/*:refs/heads/*'.freeze -  IMPORT_TAG_REFS = '+refs/tags/*:refs/tags/*'.freeze - -  def set_remote_as_mirror(name) -    # This is used to define repository as equivalent as "git clone --mirror" -    raw_repository.rugged.config["remote.#{name}.fetch"] = 'refs/*:refs/*' -    raw_repository.rugged.config["remote.#{name}.mirror"] = true -    raw_repository.rugged.config["remote.#{name}.prune"] = true -  end - -  def set_import_remote_as_mirror(remote_name) -    # Add first fetch with Rugged so it does not create its own. -    raw_repository.rugged.config["remote.#{remote_name}.fetch"] = IMPORT_HEAD_REFS - -    add_remote_fetch_config(remote_name, IMPORT_TAG_REFS) - -    raw_repository.rugged.config["remote.#{remote_name}.mirror"] = true -    raw_repository.rugged.config["remote.#{remote_name}.prune"] = true -  end - -  def add_remote_fetch_config(remote_name, refspec) -    run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}]) -  end - -  def fetch_mirror(remote, url) -    add_remote(remote, url) -    set_remote_as_mirror(remote) -    fetch_remote(remote, forced: true) -    remove_remote(remote) -  end -end diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index 5ab5c80a2f5..b3020484738 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -7,6 +7,8 @@ module Storage          raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')        end +      expires_full_path_cache +        # Move the namespace directory in all storage paths used by member projects        repository_storage_paths.each do |repository_storage_path|          # Ensure old directory exists before moving it diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb index 274b38a7708..f478c8ede18 100644 --- a/app/models/concerns/subscribable.rb +++ b/app/models/concerns/subscribable.rb @@ -13,6 +13,8 @@ module Subscribable    end    def subscribed?(user, project = nil) +    return false unless user +      if subscription = subscriptions.find_by(user: user, project: project)        subscription.subscribed      else diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index b517ddaebd7..9f403d96ed5 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -9,7 +9,7 @@ module TimeTrackable    extend ActiveSupport::Concern    included do -    attr_reader :time_spent, :time_spent_user +    attr_reader :time_spent, :time_spent_user, :spent_at      alias_method :time_spent?, :time_spent @@ -24,6 +24,7 @@ module TimeTrackable    def spend_time(options)      @time_spent = options[:duration]      @time_spent_user = options[:user] +    @spent_at = options[:spent_at]      @original_total_time_spent = nil      return if @time_spent == 0 @@ -55,7 +56,11 @@ module TimeTrackable    end    def add_or_subtract_spent_time -    timelogs.new(time_spent: time_spent, user: @time_spent_user) +    timelogs.new( +      time_spent: time_spent, +      user: @time_spent_user, +      spent_at: @spent_at +    )    end    def check_negative_time_spent diff --git a/app/models/email.rb b/app/models/email.rb index 384f38f2db7..2da8b050149 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -14,6 +14,8 @@ class Email < ActiveRecord::Base    devise :confirmable    self.reconfirmable = false  # currently email can't be changed, no need to reconfirm +  delegate :username, to: :user +    def email=(value)      write_attribute(:email, value.downcase.strip)    end diff --git a/app/models/environment.rb b/app/models/environment.rb index b6868ccbe8f..21a028e351c 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -30,7 +30,6 @@ class Environment < ActiveRecord::Base                        message: Gitlab::Regex.environment_slug_regex_message }    validates :external_url, -            uniqueness: { scope: :project_id },              length: { maximum: 255 },              allow_nil: true,              addressable_url: true @@ -110,7 +109,7 @@ class Environment < ActiveRecord::Base    end    def ref_path -    "refs/#{Repository::REF_ENVIRONMENTS}/#{Shellwords.shellescape(name)}" +    "refs/#{Repository::REF_ENVIRONMENTS}/#{slug}"    end    def formatted_external_url @@ -164,6 +163,10 @@ class Environment < ActiveRecord::Base      end    end +  def slug +    super.presence || generate_slug +  end +    # An environment name is not necessarily suitable for use in URLs, DNS    # or other third-party contexts, so provide a slugified version. A slug has    # the following properties: diff --git a/app/models/epic.rb b/app/models/epic.rb new file mode 100644 index 00000000000..62898a02e2d --- /dev/null +++ b/app/models/epic.rb @@ -0,0 +1,7 @@ +# Placeholder class for model that is implemented in EE +# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE +class Epic < ActiveRecord::Base +  # TODO: this will be implemented as part of #3853 +  def to_reference +  end +end diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb index 218e37a5312..7f1728e8c77 100644 --- a/app/models/fork_network.rb +++ b/app/models/fork_network.rb @@ -12,4 +12,8 @@ class ForkNetwork < ActiveRecord::Base    def find_forks_in(other_projects)      projects.where(id: other_projects)    end + +  def merge_requests +    MergeRequest.where(target_project: projects) +  end  end diff --git a/app/models/gcp/cluster.rb b/app/models/gcp/cluster.rb deleted file mode 100644 index 18bd6a6dcb4..00000000000 --- a/app/models/gcp/cluster.rb +++ /dev/null @@ -1,113 +0,0 @@ -module Gcp -  class Cluster < ActiveRecord::Base -    extend Gitlab::Gcp::Model -    include Presentable - -    belongs_to :project, inverse_of: :cluster -    belongs_to :user -    belongs_to :service - -    default_value_for :gcp_cluster_zone, 'us-central1-a' -    default_value_for :gcp_cluster_size, 3 -    default_value_for :gcp_machine_type, 'n1-standard-4' - -    attr_encrypted :password, -      mode: :per_attribute_iv, -      key: Gitlab::Application.secrets.db_key_base, -      algorithm: 'aes-256-cbc' - -    attr_encrypted :kubernetes_token, -      mode: :per_attribute_iv, -      key: Gitlab::Application.secrets.db_key_base, -      algorithm: 'aes-256-cbc' - -    attr_encrypted :gcp_token, -      mode: :per_attribute_iv, -      key: Gitlab::Application.secrets.db_key_base, -      algorithm: 'aes-256-cbc' - -    validates :gcp_project_id, -      length: 1..63, -      format: { -        with: Gitlab::Regex.kubernetes_namespace_regex, -        message: Gitlab::Regex.kubernetes_namespace_regex_message -      } - -    validates :gcp_cluster_name, -      length: 1..63, -      format: { -        with: Gitlab::Regex.kubernetes_namespace_regex, -        message: Gitlab::Regex.kubernetes_namespace_regex_message -      } - -    validates :gcp_cluster_zone, presence: true - -    validates :gcp_cluster_size, -      presence: true, -      numericality: { -        only_integer: true, -        greater_than: 0 -      } - -    validates :project_namespace, -      allow_blank: true, -      length: 1..63, -      format: { -        with: Gitlab::Regex.kubernetes_namespace_regex, -        message: Gitlab::Regex.kubernetes_namespace_regex_message -      } - -    # if we do not do status transition we prevent change -    validate :restrict_modification, on: :update, unless: :status_changed? - -    state_machine :status, initial: :scheduled do -      state :scheduled, value: 1 -      state :creating, value: 2 -      state :created, value: 3 -      state :errored, value: 4 - -      event :make_creating do -        transition any - [:creating] => :creating -      end - -      event :make_created do -        transition any - [:created] => :created -      end - -      event :make_errored do -        transition any - [:errored] => :errored -      end - -      before_transition any => [:errored, :created] do |cluster| -        cluster.gcp_token = nil -        cluster.gcp_operation_id = nil -      end - -      before_transition any => [:errored] do |cluster, transition| -        status_reason = transition.args.first -        cluster.status_reason = status_reason if status_reason -      end -    end - -    def project_namespace_placeholder -      "#{project.path}-#{project.id}" -    end - -    def on_creation? -      scheduled? || creating? -    end - -    def api_url -      'https://' + endpoint if endpoint -    end - -    def restrict_modification -      if on_creation? -        errors.add(:base, "cannot modify during creation") -        return false -      end - -      true -    end -  end -end diff --git a/app/models/group.rb b/app/models/group.rb index e746e4a12c9..8cf632fb566 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -6,6 +6,8 @@ class Group < Namespace    include Avatarable    include Referable    include SelectForProjectAuthorization +  include LoadedInGroupList +  include GroupDescendant    has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent    alias_method :members, :group_members @@ -24,6 +26,7 @@ class Group < Namespace    has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent    has_many :labels, class_name: 'GroupLabel'    has_many :variables, class_name: 'Ci::GroupVariable' +  has_many :custom_attributes, class_name: 'GroupCustomAttribute'    validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }    validate :visibility_level_allowed_by_projects @@ -40,6 +43,7 @@ class Group < Namespace    after_create :post_create_hook    after_destroy :post_destroy_hook    after_save :update_two_factor_requirement +  after_update :path_changed_hook, if: :path_changed?    class << self      def supports_nested_groups? @@ -178,6 +182,12 @@ class Group < Namespace      add_user(user, :owner, current_user: current_user)    end +  def member?(user, min_access_level = Gitlab::Access::GUEST) +    return false unless user + +    max_member_access_for_user(user) >= min_access_level +  end +    def has_owner?(user)      return false unless user @@ -287,6 +297,12 @@ class Group < Namespace      list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten    end +  def full_path_was +    return path_was unless has_parent? + +    "#{parent.full_path}/#{path_was}" +  end +    private    def update_two_factor_requirement @@ -295,6 +311,10 @@ class Group < Namespace      users.find_each(&:update_two_factor_requirement)    end +  def path_changed_hook +    system_hook_service.execute_hooks_for(self, :rename) +  end +    def visibility_level_allowed_by_parent      return if visibility_level_allowed_by_parent? diff --git a/app/models/group_custom_attribute.rb b/app/models/group_custom_attribute.rb new file mode 100644 index 00000000000..8157d602d67 --- /dev/null +++ b/app/models/group_custom_attribute.rb @@ -0,0 +1,6 @@ +class GroupCustomAttribute < ActiveRecord::Base +  belongs_to :group + +  validates :group, :key, :value, presence: true +  validates :key, uniqueness: { scope: [:group_id] } +end diff --git a/app/models/identity.rb b/app/models/identity.rb index 920a25932b4..ac8094b610e 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -7,7 +7,10 @@ class Identity < ActiveRecord::Base    validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }    validates :user_id, uniqueness: { scope: :provider } -  scope :with_extern_uid, ->(provider, extern_uid) { where(extern_uid: extern_uid, provider: provider) } +  scope :with_extern_uid, ->(provider, extern_uid) do +    extern_uid = Gitlab::LDAP::Person.normalize_dn(extern_uid) if provider.starts_with?('ldap') +    where(extern_uid: extern_uid, provider: provider) +  end    def ldap?      provider.starts_with?('ldap') diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb new file mode 100644 index 00000000000..b30b707e5fe --- /dev/null +++ b/app/models/instance_configuration.rb @@ -0,0 +1,71 @@ +require 'resolv' + +class InstanceConfiguration +  SSH_ALGORITHMS = %w(DSA ECDSA ED25519 RSA).freeze +  SSH_ALGORITHMS_PATH = '/etc/ssh/'.freeze +  CACHE_KEY = 'instance_configuration'.freeze +  EXPIRATION_TIME = 24.hours + +  def settings +    @configuration ||= Rails.cache.fetch(CACHE_KEY, expires_in: EXPIRATION_TIME) do +      { ssh_algorithms_hashes: ssh_algorithms_hashes, +        host: host, +        gitlab_pages: gitlab_pages, +        gitlab_ci: gitlab_ci }.deep_symbolize_keys +    end +  end + +  private + +  def ssh_algorithms_hashes +    SSH_ALGORITHMS.map { |algo| ssh_algorithm_hashes(algo) }.compact +  end + +  def host +    Settings.gitlab.host +  end + +  def gitlab_pages +    Settings.pages.to_h.merge(ip_address: resolv_dns(Settings.pages.host)) +  end + +  def resolv_dns(dns) +    Resolv.getaddress(dns) +  rescue Resolv::ResolvError +  end + +  def gitlab_ci +    Settings.gitlab_ci +            .to_h +            .merge(artifacts_max_size: { value: Settings.artifacts.max_size&.megabytes, +                                         default: 100.megabytes }) +  end + +  def ssh_algorithm_file(algorithm) +    File.join(SSH_ALGORITHMS_PATH, "ssh_host_#{algorithm.downcase}_key.pub") +  end + +  def ssh_algorithm_hashes(algorithm) +    content = ssh_algorithm_file_content(algorithm) +    return unless content.present? + +    { name: algorithm, +      md5: ssh_algorithm_md5(content), +      sha256: ssh_algorithm_sha256(content) } +  end + +  def ssh_algorithm_file_content(algorithm) +    file = ssh_algorithm_file(algorithm) +    return unless File.exist?(file) + +    File.read(file) +  end + +  def ssh_algorithm_md5(ssh_file_content) +    OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':') +  end + +  def ssh_algorithm_sha256(ssh_file_content) +    OpenSSL::Digest::SHA256.hexdigest(ssh_file_content) +  end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 155c5d972b7..3b3c7fb7f8b 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -5,11 +5,10 @@ class Issue < ActiveRecord::Base    include Issuable    include Noteable    include Referable -  include Sortable    include Spammable    include FasterCacheKeys    include RelativePositioning -  include CreatedAtFilterable +  include TimeTrackable    DueDateStruct = Struct.new(:title, :name).freeze    NoDueDate     = DueDateStruct.new('No Due Date', '0').freeze @@ -74,19 +73,7 @@ class Issue < ActiveRecord::Base      end    end -  def hook_attrs -    assignee_ids = self.assignee_ids - -    attrs = { -      total_time_spent: total_time_spent, -      human_total_time_spent: human_total_time_spent, -      human_time_estimate: human_time_estimate, -      assignee_ids: assignee_ids, -      assignee_id: assignee_ids.first # This key is deprecated -    } - -    attributes.merge!(attrs) -  end +  acts_as_paranoid    def self.reference_prefix      '#' @@ -131,6 +118,10 @@ class Issue < ActiveRecord::Base                "id DESC")    end +  def hook_attrs +    Gitlab::HookData::IssueBuilder.new(self).build +  end +    # Returns a Hash of attributes to be used for Twitter card metadata    def card_attributes      { diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 292122f779e..82d0ae90d77 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -3,11 +3,11 @@ class MergeRequest < ActiveRecord::Base    include Issuable    include Noteable    include Referable -  include Sortable    include IgnorableColumn -  include CreatedAtFilterable +  include TimeTrackable -  ignore_column :locked_at +  ignore_column :locked_at, +                :ref_fetched    belongs_to :target_project, class_name: "Project"    belongs_to :source_project, class_name: "Project" @@ -119,6 +119,8 @@ class MergeRequest < ActiveRecord::Base    after_save :keep_around_commit +  acts_as_paranoid +    def self.reference_prefix      '!'    end @@ -179,6 +181,10 @@ class MergeRequest < ActiveRecord::Base      work_in_progress?(title) ? title : "WIP: #{title}"    end +  def hook_attrs +    Gitlab::HookData::MergeRequestBuilder.new(self).build +  end +    # Returns a Hash of attributes to be used for Twitter card metadata    def card_attributes      { @@ -392,7 +398,11 @@ class MergeRequest < ActiveRecord::Base    end    def merge_ongoing? -    !!merge_jid && !merged? +    # While the MergeRequest is locked, it should present itself as 'merge ongoing'. +    # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron. +    return true if locked? + +    !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid)    end    def closed_without_fork? @@ -415,7 +425,7 @@ class MergeRequest < ActiveRecord::Base    end    def create_merge_request_diff -    fetch_ref +    fetch_ref!      # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435      Gitlab::GitalyClient.allow_n_plus_1_calls do @@ -587,24 +597,6 @@ class MergeRequest < ActiveRecord::Base      !discussions_to_be_resolved?    end -  def hook_attrs -    attrs = { -      source: source_project.try(:hook_attrs), -      target: target_project.hook_attrs, -      last_commit: nil, -      work_in_progress: work_in_progress?, -      total_time_spent: total_time_spent, -      human_total_time_spent: human_total_time_spent, -      human_time_estimate: human_time_estimate -    } - -    if diff_head_commit -      attrs[:last_commit] = diff_head_commit.hook_attrs -    end - -    attributes.merge!(attrs) -  end -    def for_fork?      target_project != source_project    end @@ -689,13 +681,13 @@ class MergeRequest < ActiveRecord::Base    def source_branch_exists?      return false unless self.source_project -    self.source_project.repository.branch_names.include?(self.source_branch) +    self.source_project.repository.branch_exists?(self.source_branch)    end    def target_branch_exists?      return false unless self.target_project -    self.target_project.repository.branch_names.include?(self.target_branch) +    self.target_project.repository.branch_exists?(self.target_branch)    end    def merge_commit_message(include_description: false) @@ -818,29 +810,14 @@ class MergeRequest < ActiveRecord::Base      end    end -  def fetch_ref -    write_ref -    update_column(:ref_fetched, true) +  def fetch_ref! +    target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)    end    def ref_path      "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"    end -  def ref_fetched? -    super || -      begin -        computed_value = project.repository.ref_exists?(ref_path) -        update_column(:ref_fetched, true) if computed_value - -        computed_value -      end -  end - -  def ensure_ref_fetched -    fetch_ref unless ref_fetched? -  end -    def in_locked_state      begin        lock_mr @@ -888,7 +865,7 @@ class MergeRequest < ActiveRecord::Base    #    def all_commit_shas      if persisted? -      column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).pluck('DISTINCT(sha)') +      column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).limit(10_000).pluck('sha')        serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)        (column_shas + serialised_shas).uniq @@ -982,10 +959,4 @@ class MergeRequest < ActiveRecord::Base      project.merge_requests.merged.where(author_id: author_id).empty?    end - -  private - -  def write_ref -    target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path) -  end  end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index faf0b95f842..1eda0f9cbbd 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -48,6 +48,10 @@ class MergeRequestDiff < ActiveRecord::Base    # Collect information about commits and diff from repository    # and save it to the database as serialized data    def save_git_content +    MergeRequest +      .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id) +      .update_all(latest_merge_request_diff_id: self.id) +      ensure_commit_shas      save_commits      save_diffs diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb index 670b26d4ca3..b75387e236e 100644 --- a/app/models/merge_request_diff_commit.rb +++ b/app/models/merge_request_diff_commit.rb @@ -17,7 +17,9 @@ class MergeRequestDiffCommit < ActiveRecord::Base        commit_hash.merge(          merge_request_diff_id: merge_request_diff_id,          relative_order: index, -        sha: sha_attribute.type_cast_for_database(sha) +        sha: sha_attribute.type_cast_for_database(sha), +        authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]), +        committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date])        )      end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 4672881e220..4d401e7ba18 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -36,7 +36,7 @@ class Namespace < ActiveRecord::Base    validates :path,      presence: true,      length: { maximum: 255 }, -    dynamic_path: true +    namespace_path: true    validate :nesting_level_allowed @@ -162,6 +162,13 @@ class Namespace < ActiveRecord::Base        .base_and_ancestors    end +  # returns all ancestors upto but excluding the the given namespace +  # when no namespace is given, all ancestors upto the top are returned +  def ancestors_upto(top = nil) +    Gitlab::GroupHierarchy.new(self.class.where(id: id)) +      .ancestors(upto: top) +  end +    def self_and_ancestors      return self.class.where(id: id) unless parent_id diff --git a/app/models/note.rb b/app/models/note.rb index ceded9f2aef..f9676361072 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -69,7 +69,7 @@ class Note < ActiveRecord::Base    delegate :title, to: :noteable, allow_nil: true    validates :note, presence: true -  validates :project, presence: true, unless: :for_personal_snippet? +  validates :project, presence: true, if: :for_project_noteable?    # Attachments are deprecated and are handled by Markdown uploader    validates :attachment, file_size: { maximum: :max_attachment_size } @@ -114,7 +114,7 @@ class Note < ActiveRecord::Base    after_initialize :ensure_discussion_id    before_validation :nullify_blank_type, :nullify_blank_line_code    before_validation :set_discussion_id, on: :create -  after_save :keep_around_commit, unless: :for_personal_snippet? +  after_save :keep_around_commit, if: :for_project_noteable?    after_save :expire_etag_cache    after_destroy :expire_etag_cache @@ -169,7 +169,7 @@ class Note < ActiveRecord::Base    end    def cross_reference? -    system? && SystemNoteService.cross_reference?(note) +    system? && matches_cross_reference_regex?    end    def diff_note? @@ -208,6 +208,10 @@ class Note < ActiveRecord::Base      noteable.is_a?(PersonalSnippet)    end +  def for_project_noteable? +    !for_personal_snippet? +  end +    def skip_project_check?      for_personal_snippet?    end diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb index b85f5dbaf2e..e8595b13d6d 100644 --- a/app/models/oauth_access_token.rb +++ b/app/models/oauth_access_token.rb @@ -1,4 +1,14 @@  class OauthAccessToken < Doorkeeper::AccessToken    belongs_to :resource_owner, class_name: 'User'    belongs_to :application, class_name: 'Doorkeeper::Application' + +  alias_attribute :user, :resource_owner + +  def scopes=(value) +    if value.is_a?(Array) +      super(Doorkeeper::OAuth::Scopes.from_array(value).to_s) +    else +      super +    end +  end  end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 5d798247863..2e824cda525 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -16,9 +16,9 @@ class PagesDomain < ActiveRecord::Base      key: Gitlab::Application.secrets.db_key_base,      algorithm: 'aes-256-cbc' -  after_create :update -  after_save :update -  after_destroy :update +  after_create :update_daemon +  after_save :update_daemon +  after_destroy :update_daemon    def to_param      domain @@ -80,7 +80,7 @@ class PagesDomain < ActiveRecord::Base    private -  def update +  def update_daemon      ::Projects::UpdatePagesConfigurationService.new(project).execute    end diff --git a/app/models/project.rb b/app/models/project.rb index 57e91ab3b88..53df29dab02 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -17,6 +17,7 @@ class Project < ActiveRecord::Base    include ProjectFeaturesCompatibility    include SelectForProjectAuthorization    include Routable +  include GroupDescendant    extend Gitlab::ConfigHelper    extend Gitlab::CurrentSettings @@ -25,7 +26,15 @@ class Project < ActiveRecord::Base    NUMBER_OF_PERMITTED_BOARDS = 1    UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze -  LATEST_STORAGE_VERSION = 1 +  # Hashed Storage versions handle rolling out new storage to project and dependents models: +  # nil: legacy +  # 1: repository +  # 2: attachments +  LATEST_STORAGE_VERSION = 2 +  HASHED_STORAGE_FEATURES = { +    repository: 1, +    attachments: 2 +  }.freeze    cache_markdown_field :description, pipeline: :description @@ -81,6 +90,8 @@ class Project < ActiveRecord::Base    belongs_to :creator, class_name: 'User'    belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'    belongs_to :namespace +  alias_method :parent, :namespace +  alias_attribute :parent_id, :namespace_id    has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'    has_many :boards, before_add: :validate_board_limit @@ -117,6 +128,7 @@ class Project < ActiveRecord::Base    has_one :mock_deployment_service    has_one :mock_monitoring_service    has_one :microsoft_teams_service +  has_one :packagist_service    # TODO: replace these relations with the fork network versions    has_one  :forked_project_link,  foreign_key: "forked_to_project_id" @@ -174,7 +186,10 @@ class Project < ActiveRecord::Base    has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true    has_one :project_feature, inverse_of: :project    has_one :statistics, class_name: 'ProjectStatistics' -  has_one :cluster, class_name: 'Gcp::Cluster', inverse_of: :project + +  has_one :cluster_project, class_name: 'Clusters::Project' +  has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster' +  has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'    # Container repositories need to remove data from the container registry,    # which is not managed by the DB. Hence we're still using dependent: :destroy @@ -201,6 +216,7 @@ class Project < ActiveRecord::Base    has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'    has_one :auto_devops, class_name: 'ProjectAutoDevops' +  has_many :custom_attributes, class_name: 'ProjectCustomAttribute'    accepts_nested_attributes_for :variables, allow_destroy: true    accepts_nested_attributes_for :project_feature, update_only: true @@ -228,10 +244,8 @@ class Project < ActiveRecord::Base                message: Gitlab::Regex.project_name_regex_message }    validates :path,      presence: true, -    dynamic_path: true, +    project_path: true,      length: { maximum: 255 }, -    format: { with: Gitlab::PathRegex.project_path_format_regex, -              message: Gitlab::PathRegex.project_path_format_message },      uniqueness: { scope: :namespace_id }    validates :namespace, presence: true @@ -479,6 +493,13 @@ class Project < ActiveRecord::Base      end    end +  # returns all ancestor-groups upto but excluding the given namespace +  # when no namespace is given, all ancestors upto the top are returned +  def ancestors_upto(top = nil) +    Gitlab::GroupHierarchy.new(Group.where(id: namespace_id)) +      .base_and_ancestors(upto: top) +  end +    def lfs_enabled?      return namespace.lfs_enabled? if self[:lfs_enabled].nil? @@ -530,6 +551,10 @@ class Project < ActiveRecord::Base      repository.commit(ref)    end +  def commit_by(oid:) +    repository.commit_by(oid: oid) +  end +    # ref can't be HEAD, can only be branch/tag name or SHA    def latest_successful_builds_for(ref = default_branch)      latest_pipeline = pipelines.latest_successful_for(ref) @@ -543,7 +568,7 @@ class Project < ActiveRecord::Base    def merge_base_commit(first_commit_id, second_commit_id)      sha = repository.merge_base(first_commit_id, second_commit_id) -    repository.commit(sha) if sha +    commit_by(oid: sha) if sha    end    def saved? @@ -1017,6 +1042,10 @@ class Project < ActiveRecord::Base      !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)    end +  def fork_source +    forked_from_project || fork_network&.root_project +  end +    def personal?      !group    end @@ -1069,6 +1098,7 @@ class Project < ActiveRecord::Base    def hook_attrs(backward: true)      attrs = { +      id: id,        name: name,        description: description,        web_url: web_url, @@ -1262,7 +1292,7 @@ class Project < ActiveRecord::Base      # self.forked_from_project will be nil before the project is saved, so      # we need to go through the relation -    original_project = forked_project_link.forked_from_project +    original_project = forked_project_link&.forked_from_project      return true unless original_project      level <= original_project.visibility_level @@ -1380,6 +1410,19 @@ class Project < ActiveRecord::Base      end    end +  def after_rename_repo +    path_before_change = previous_changes['path'].first + +    # We need to check if project had been rolled out to move resource to hashed storage or not and decide +    # if we need execute any take action or no-op. + +    unless hashed_storage?(:attachments) +      Gitlab::UploadsTransfer.new.rename_project(path_before_change, self.path, namespace.full_path) +    end + +    Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path) +  end +    def rename_repo_notify!      send_move_instructions(full_path_was)      expires_full_path_cache @@ -1390,13 +1433,6 @@ class Project < ActiveRecord::Base      reload_repository!    end -  def after_rename_repo -    path_before_change = previous_changes['path'].first - -    Gitlab::UploadsTransfer.new.rename_project(path_before_change, self.path, namespace.full_path) -    Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path) -  end -    def running_or_pending_build_count(force: false)      Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do        builds.running_or_pending.count(:all) @@ -1458,7 +1494,8 @@ class Project < ActiveRecord::Base        { key: 'CI_PROJECT_PATH', value: full_path, public: true },        { key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true },        { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true }, -      { key: 'CI_PROJECT_URL', value: web_url, public: true } +      { key: 'CI_PROJECT_URL', value: web_url, public: true }, +      { key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level), public: true }      ]    end @@ -1549,10 +1586,6 @@ class Project < ActiveRecord::Base      map.public_path_for_source_path(path)    end -  def parent -    namespace -  end -    def parent_changed?      namespace_id_changed?    end @@ -1590,8 +1623,13 @@ class Project < ActiveRecord::Base      [nil, 0].include?(self.storage_version)    end -  def hashed_storage? -    self.storage_version && self.storage_version >= 1 +  # Check if Hashed Storage is enabled for the project with at least informed feature rolled out +  # +  # @param [Symbol] feature that needs to be rolled out for the project (:repository, :attachments) +  def hashed_storage?(feature) +    raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature) + +    self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]    end    def renamed? @@ -1627,7 +1665,7 @@ class Project < ActiveRecord::Base    end    def migrate_to_hashed_storage! -    return if hashed_storage? +    return if hashed_storage?(:repository)      update!(repository_read_only: true) @@ -1648,11 +1686,15 @@ class Project < ActiveRecord::Base      Gitlab::GlRepository.gl_repository(self, is_wiki)    end +  def reference_counter(wiki: false) +    Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki)) +  end +    private    def storage      @storage ||= -      if hashed_storage? +      if hashed_storage?(:repository)          Storage::HashedProject.new(self)        else          Storage::LegacyProject.new(self) @@ -1666,11 +1708,11 @@ class Project < ActiveRecord::Base    end    def repo_reference_count -    Gitlab::ReferenceCounter.new(gl_repository(is_wiki: false)).value +    reference_counter.value    end    def wiki_reference_count -    Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value +    reference_counter(wiki: true).value    end    def check_repository_absence! diff --git a/app/models/project_custom_attribute.rb b/app/models/project_custom_attribute.rb new file mode 100644 index 00000000000..3f1a7b86a82 --- /dev/null +++ b/app/models/project_custom_attribute.rb @@ -0,0 +1,6 @@ +class ProjectCustomAttribute < ActiveRecord::Base +  belongs_to :project + +  validates :project, :key, :value, presence: true +  validates :key, uniqueness: { scope: [:project_id] } +end diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb index 1327b075858..3273f41dbd2 100644 --- a/app/models/project_services/chat_message/issue_message.rb +++ b/app/models/project_services/chat_message/issue_message.rb @@ -39,7 +39,7 @@ module ChatMessage      private      def message -      if state == 'opened' +      if opened_issue?          "[#{project_link}] Issue #{state} by #{user_combined_name}"        else          "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}" diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 9ee3a533c1e..b487378edd2 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -3,6 +3,8 @@ class JiraService < IssueTrackerService    validates :url, url: true, presence: true, if: :activated?    validates :api_url, url: true, allow_blank: true +  validates :username, presence: true, if: :activated? +  validates :password, presence: true, if: :activated?    prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 8ba07173c74..5080acffb3c 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -136,6 +136,10 @@ class KubernetesService < DeploymentService      { pods: read_pods }    end +  def kubeclient +    @kubeclient ||= build_kubeclient! +  end +    TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze    private @@ -153,7 +157,10 @@ class KubernetesService < DeploymentService    end    def default_namespace -    "#{project.path}-#{project.id}" if project.present? +    return unless project + +    slug = "#{project.path}-#{project.id}".downcase +    slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')    end    def build_kubeclient!(api_path: 'api', api_version: 'v1') diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb new file mode 100644 index 00000000000..f68a0c1a3c3 --- /dev/null +++ b/app/models/project_services/packagist_service.rb @@ -0,0 +1,65 @@ +class PackagistService < Service +  include HTTParty + +  prop_accessor :username, :token, :server + +  validates :username, presence: true, if: :activated? +  validates :token, presence: true, if: :activated? + +  default_value_for :push_events, true +  default_value_for :tag_push_events, true + +  after_save :compose_service_hook, if: :activated? + +  def title +    'Packagist' +  end + +  def description +    'Update your project on Packagist, the main Composer repository' +  end + +  def self.to_param +    'packagist' +  end + +  def fields +    [ +      { type: 'text', name: 'username', placeholder: '', required: true }, +      { type: 'text', name: 'token', placeholder: '', required: true }, +      { type: 'text', name: 'server', placeholder: 'https://packagist.org', required: false } +    ] +  end + +  def self.supported_events +    %w(push merge_request tag_push) +  end + +  def execute(data) +    return unless supported_events.include?(data[:object_kind]) + +    service_hook.execute(data) +  end + +  def test(data) +    begin +      result = execute(data) +      return { success: false, result: result[:message] } if result[:http_status] != 202 +    rescue StandardError => error +      return { success: false, result: error } +    end + +    { success: true, result: result[:message] } +  end + +  def compose_service_hook +    hook = service_hook || build_service_hook +    hook.url = hook_url +    hook.save +  end + +  def hook_url +    base_url = server.present? ? server : 'https://packagist.org' +    "#{base_url}/api/update-package?username=#{username}&apiToken=#{token}" +  end +end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index bb7be29ef66..43de6809178 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -135,7 +135,7 @@ class ProjectWiki    end    def repository -    @repository ||= Repository.new(full_path, @project, disk_path: disk_path) +    @repository ||= Repository.new(full_path, @project, disk_path: disk_path, is_wiki: true)    end    def default_branch diff --git a/app/models/repository.rb b/app/models/repository.rb index d725c65081d..eb7766d040c 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -15,9 +15,8 @@ class Repository    ].freeze    include Gitlab::ShellAdapter -  include RepositoryMirroring -  attr_accessor :full_path, :disk_path, :project +  attr_accessor :full_path, :disk_path, :project, :is_wiki    delegate :ref_name_for_sha, to: :raw_repository @@ -34,7 +33,8 @@ class Repository    CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide                        changelog license_blob license_key gitignore koding_yml                        gitlab_ci_yml branch_names tag_names branch_count -                      tag_count avatar exists? empty? root_ref has_visible_content?).freeze +                      tag_count avatar exists? empty? root_ref has_visible_content? +                      issue_template_names merge_request_template_names).freeze    # Methods that use cache_method but only memoize the value    MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze @@ -50,7 +50,9 @@ class Repository      gitignore: :gitignore,      koding: :koding_yml,      gitlab_ci: :gitlab_ci_yml, -    avatar: :avatar +    avatar: :avatar, +    issue_template: :issue_template_names, +    merge_request_template: :merge_request_template_names    }.freeze    # Wraps around the given method and caches its output in Redis and an instance @@ -69,10 +71,12 @@ class Repository      end    end -  def initialize(full_path, project, disk_path: nil) +  def initialize(full_path, project, disk_path: nil, is_wiki: false)      @full_path = full_path      @disk_path = disk_path || full_path      @project = project +    @commit_cache = {} +    @is_wiki = is_wiki    end    def ==(other) @@ -100,18 +104,17 @@ class Repository    def commit(ref = 'HEAD')      return nil unless exists? +    return ref if ref.is_a?(::Commit) -    commit = -      if ref.is_a?(Gitlab::Git::Commit) -        ref -      else -        Gitlab::Git::Commit.find(raw_repository, ref) -      end +    find_commit(ref) +  end -    commit = ::Commit.new(commit, @project) if commit -    commit -  rescue Rugged::OdbError, Rugged::TreeError -    nil +  # Finding a commit by the passed SHA +  # Also takes care of caching, based on the SHA +  def commit_by(oid:) +    return @commit_cache[oid] if @commit_cache.key?(oid) + +    @commit_cache[oid] = find_commit(oid)    end    def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil) @@ -228,7 +231,7 @@ class Repository    # branches or tags, but we want to keep some of these commits around, for    # example if they have comments or CI builds.    def keep_around(sha) -    return unless sha && commit(sha) +    return unless sha && commit_by(oid: sha)      return if kept_around?(sha) @@ -465,9 +468,7 @@ class Repository    end    def blob_at(sha, path) -    unless Gitlab::Git.blank_ref?(sha) -      Blob.decorate(Gitlab::Git::Blob.find(self, sha, path), project) -    end +    Blob.decorate(raw_repository.blob_at(sha, path), project)    rescue Gitlab::Git::Repository::NoRepository      nil    end @@ -535,6 +536,16 @@ class Repository    end    cache_method :avatar +  def issue_template_names +    Gitlab::Template::IssueTemplate.dropdown_names(project) +  end +  cache_method :issue_template_names, fallback: [] + +  def merge_request_template_names +    Gitlab::Template::MergeRequestTemplate.dropdown_names(project) +  end +  cache_method :merge_request_template_names, fallback: [] +    def readme      if readme = tree(:head)&.readme        ReadmeBlob.new(readme, self) @@ -851,22 +862,12 @@ class Repository    end    def ff_merge(user, source, target_branch, merge_request: nil) -    our_commit = rugged.branches[target_branch].target -    their_commit = -      if source.is_a?(Gitlab::Git::Commit) -        source.raw_commit -      else -        rugged.lookup(source) -      end - -    raise 'Invalid merge target' if our_commit.nil? -    raise 'Invalid merge source' if their_commit.nil? +    their_commit_id = commit(source)&.id +    raise 'Invalid merge source' if their_commit_id.nil? -    with_branch(user, target_branch) do |start_commit| -      merge_request&.update(in_progress_merge_commit_sha: their_commit.oid) +    merge_request&.update(in_progress_merge_commit_sha: their_commit_id) -      their_commit.oid -    end +    with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }    end    def revert( @@ -901,26 +902,27 @@ class Repository      end    end -  def resolve_conflicts(user, branch_name, params) -    with_branch(user, branch_name) do -      committer = user_to_committer(user) +  def merged_to_root_ref?(branch_or_name, pre_loaded_merged_branches = nil) +    branch = Gitlab::Git::Branch.find(self, branch_or_name) -      create_commit(params.merge(author: committer, committer: committer)) -    end -  end - -  def merged_to_root_ref?(branch_name) -    branch_commit = commit(branch_name) -    root_ref_commit = commit(root_ref) +    if branch +      @root_ref_sha ||= commit(root_ref).sha +      same_head = branch.target == @root_ref_sha +      merged = +        if pre_loaded_merged_branches +          pre_loaded_merged_branches.include?(branch.name) +        else +          ancestor?(branch.target, @root_ref_sha) +        end -    if branch_commit -      same_head = branch_commit.id == root_ref_commit.id -      !same_head && ancestor?(branch_commit.id, root_ref_commit.id) +      !same_head && merged      else        nil      end    end +  delegate :merged_branch_names, to: :raw_repository +    def merge_base(first_commit_id, second_commit_id)      first_commit_id = commit(first_commit_id).try(:id) || first_commit_id      second_commit_id = commit(second_commit_id).try(:id) || second_commit_id @@ -963,25 +965,12 @@ class Repository      run_git(args).first.lines.map(&:strip)    end -  def add_remote(name, url) -    raw_repository.remote_add(name, url) -  rescue Rugged::ConfigError -    raw_repository.remote_update(name, url: url) +  def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false) +    gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)    end -  def remove_remote(name) -    raw_repository.remote_delete(name) -    true -  rescue Rugged::ConfigError -    false -  end - -  def fetch_remote(remote, forced: false, no_tags: false) -    gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags) -  end - -  def fetch_source_branch(source_repository, source_branch, local_ref) -    raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref) +  def fetch_source_branch!(source_repository, source_branch, local_ref) +    raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)    end    def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:) @@ -1028,6 +1017,10 @@ class Repository      if instance_variable_defined?(ivar)        instance_variable_get(ivar)      else +      # If the repository doesn't exist and a fallback was specified we return +      # that value inmediately. This saves us Rugged/gRPC invocations. +      return fallback unless fallback.nil? || exists? +        begin          value =            if memoize_only @@ -1037,8 +1030,9 @@ class Repository            end          instance_variable_set(ivar, value)        rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository -        # if e.g. HEAD or the entire repository doesn't exist we want to -        # gracefully handle this and not cache anything. +        # Even if the above `#exists?` check passes these errors might still +        # occur (for example because of a non-existing HEAD). We want to +        # gracefully handle this and not cache anything          fallback        end      end @@ -1066,6 +1060,18 @@ class Repository    private +  # TODO Generice finder, later split this on finders by Ref or Oid +  # gitlab-org/gitlab-ce#39239 +  def find_commit(oid_or_ref) +    commit = if oid_or_ref.is_a?(Gitlab::Git::Commit) +               oid_or_ref +             else +               Gitlab::Git::Commit.find(raw_repository, oid_or_ref) +             end + +    ::Commit.new(commit, @project) if commit +  end +    def blob_data_at(sha, path)      blob = blob_at(sha, path)      return unless blob @@ -1104,17 +1110,17 @@ class Repository    def last_commit_for_path_by_gitaly(sha, path)      c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path) -    commit(c) +    commit_by(oid: c)    end    def last_commit_for_path_by_rugged(sha, path)      sha = last_commit_id_for_path_by_shelling_out(sha, path) -    commit(sha) +    commit_by(oid: sha)    end    def last_commit_id_for_path_by_shelling_out(sha, path)      args = %W(rev-list --max-count=1 #{sha} -- #{path}) -    run_git(args).first.strip +    raw_repository.run_git_with_timeout(args, Gitlab::Git::Popen::FAST_GIT_PROCESS_TIMEOUT).first.strip    end    def repository_storage_path @@ -1122,7 +1128,7 @@ class Repository    end    def initialize_raw_repository -    Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false)) +    Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki))    end    def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) diff --git a/app/models/service.rb b/app/models/service.rb index 6b64079215f..fdd2605e3e3 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -238,6 +238,7 @@ class Service < ActiveRecord::Base        kubernetes        mattermost_slash_commands        mattermost +      packagist        pipelines_email        pivotaltracker        prometheus diff --git a/app/models/user.rb b/app/models/user.rb index 533a776bc65..aa88cda4dc0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -21,8 +21,8 @@ class User < ActiveRecord::Base    ignore_column :external_email    ignore_column :email_provider +  ignore_column :authentication_token -  add_authentication_token_field :authentication_token    add_authentication_token_field :incoming_email_token    add_authentication_token_field :rss_token @@ -146,7 +146,7 @@ class User < ActiveRecord::Base      presence: true,      numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }    validates :username, -    dynamic_path: true, +    user_path: true,      presence: true,      uniqueness: { case_sensitive: false } @@ -163,11 +163,12 @@ class User < ActiveRecord::Base    before_validation :sanitize_attrs    before_validation :set_notification_email, if: :email_changed?    before_validation :set_public_email, if: :public_email_changed? -  before_save :ensure_authentication_token, :ensure_incoming_email_token -  before_save :ensure_user_rights_and_limits, if: :external_changed? +  before_save :ensure_incoming_email_token +  before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }    before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }    before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }    after_save :ensure_namespace_correct +  after_update :username_changed_hook, if: :username_changed?    after_destroy :post_destroy_hook    after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }    after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') } @@ -182,15 +183,8 @@ class User < ActiveRecord::Base    enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]    # User's Project preference -  # -  # Note: When adding an option, it MUST go on the end of the hash with a -  # number higher than the current max. We cannot move options and/or change -  # their numbers. -  # -  # We skip 0 because this was used by an option that has since been removed. -  enum project_view: { activity: 1, files: 2 } - -  alias_attribute :private_token, :authentication_token +  # Note: When adding an option, it MUST go on the end of the array. +  enum project_view: [:readme, :activity, :files]    delegate :path, to: :namespace, allow_nil: true, prefix: true @@ -878,6 +872,10 @@ class User < ActiveRecord::Base      end    end +  def username_changed_hook +    system_hook_service.execute_hooks_for(self, :rename) +  end +    def post_destroy_hook      log_info("User \"#{name}\" (#{email})  was removed")      system_hook_service.execute_hooks_for(self, :destroy) @@ -1141,8 +1139,9 @@ class User < ActiveRecord::Base        self.can_create_group = false        self.projects_limit   = 0      else -      self.can_create_group = gitlab_config.default_can_create_group -      self.projects_limit = current_application_settings.default_projects_limit +      # Only revert these back to the default if they weren't specifically changed in this update. +      self.can_create_group = gitlab_config.default_can_create_group unless can_create_group_changed? +      self.projects_limit = current_application_settings.default_projects_limit unless projects_limit_changed?      end    end diff --git a/app/policies/gcp/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb index e77173ea6e1..1f7c13072b9 100644 --- a/app/policies/gcp/cluster_policy.rb +++ b/app/policies/clusters/cluster_policy.rb @@ -1,8 +1,8 @@ -module Gcp +module Clusters    class ClusterPolicy < BasePolicy      alias_method :cluster, :subject -    delegate { @subject.project } +    delegate { cluster.project }      rule { can?(:master_access) }.policy do        enable :update_cluster diff --git a/app/presenters/gcp/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb index f7908f92a37..01cb59d0d44 100644 --- a/app/presenters/gcp/cluster_presenter.rb +++ b/app/presenters/clusters/cluster_presenter.rb @@ -1,9 +1,9 @@ -module Gcp +module Clusters    class ClusterPresenter < Gitlab::View::Presenter::Delegated      presents :cluster      def gke_cluster_url -      "https://console.cloud.google.com/kubernetes/clusters/details/#{gcp_cluster_zone}/#{gcp_cluster_name}" +      "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?      end    end  end diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb index 4e6c15f673b..8cade280b0c 100644 --- a/app/serializers/base_serializer.rb +++ b/app/serializers/base_serializer.rb @@ -1,6 +1,9 @@  class BaseSerializer -  def initialize(parameters = {}) -    @request = EntityRequest.new(parameters) +  attr_reader :params + +  def initialize(params = {}) +    @params = params +    @request = EntityRequest.new(params)    end    def represent(resource, opts = {}, entity_class = nil) diff --git a/app/serializers/blob_entity.rb b/app/serializers/blob_entity.rb index 56f173e5a27..ad039a2623d 100644 --- a/app/serializers/blob_entity.rb +++ b/app/serializers/blob_entity.rb @@ -3,10 +3,6 @@ class BlobEntity < Grape::Entity    expose :id, :path, :name, :mode -  expose :last_commit do |blob| -    request.project.repository.last_commit_for_path(blob.commit_id, blob.path) -  end -    expose :icon do |blob|      IconsHelper.file_type_icon_class('file', blob.mode, blob.name)    end diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb new file mode 100644 index 00000000000..3f9a275ad08 --- /dev/null +++ b/app/serializers/cluster_application_entity.rb @@ -0,0 +1,5 @@ +class ClusterApplicationEntity < Grape::Entity +  expose :name +  expose :status_name, as: :status +  expose :status_reason +end diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb index 08a113c4d8a..7e5b0997878 100644 --- a/app/serializers/cluster_entity.rb +++ b/app/serializers/cluster_entity.rb @@ -3,4 +3,5 @@ class ClusterEntity < Grape::Entity    expose :status_name, as: :status    expose :status_reason +  expose :applications, using: ClusterApplicationEntity  end diff --git a/app/serializers/cluster_serializer.rb b/app/serializers/cluster_serializer.rb index 2c87202a105..2e13c1501e7 100644 --- a/app/serializers/cluster_serializer.rb +++ b/app/serializers/cluster_serializer.rb @@ -2,6 +2,6 @@ class ClusterSerializer < BaseSerializer    entity ClusterEntity    def represent_status(resource) -    represent(resource, { only: [:status, :status_reason] }) +    represent(resource, { only: [:status, :status_reason, :applications] })    end  end diff --git a/app/serializers/concerns/with_pagination.rb b/app/serializers/concerns/with_pagination.rb new file mode 100644 index 00000000000..d29e22d6740 --- /dev/null +++ b/app/serializers/concerns/with_pagination.rb @@ -0,0 +1,22 @@ +module WithPagination +  attr_accessor :paginator + +  def with_pagination(request, response) +    tap { self.paginator = Gitlab::Serializer::Pagination.new(request, response) } +  end + +  def paginated? +    paginator.present? +  end + +  # super is `BaseSerializer#represent` here. +  # +  # we shouldn't try to paginate single resources +  def represent(resource, opts = {}) +    if paginated? && resource.respond_to?(:page) +      super(@paginator.paginate(resource), opts) +    else +      super(resource, opts) +    end +  end +end diff --git a/app/serializers/container_tag_entity.rb b/app/serializers/container_tag_entity.rb index ec1fc349586..8f1488e6cbb 100644 --- a/app/serializers/container_tag_entity.rb +++ b/app/serializers/container_tag_entity.rb @@ -1,10 +1,10 @@  class ContainerTagEntity < Grape::Entity    include RequestAwareEntity -  expose :name, :location, :revision, :total_size, :created_at +  expose :name, :location, :revision, :short_revision, :total_size, :created_at    expose :destroy_path, if: -> (*) { can_destroy? } do |tag| -    project_registry_repository_tag_path(project, tag.repository, tag.name, format: :json) +    project_registry_repository_tag_path(project, tag.repository, tag.name)    end    private diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb index 88842a9aa75..84722f33f59 100644 --- a/app/serializers/environment_serializer.rb +++ b/app/serializers/environment_serializer.rb @@ -1,4 +1,6 @@  class EnvironmentSerializer < BaseSerializer +  include WithPagination +    Item = Struct.new(:name, :size, :latest)    entity EnvironmentEntity @@ -7,18 +9,10 @@ class EnvironmentSerializer < BaseSerializer      tap { @itemize = true }    end -  def with_pagination(request, response) -    tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) } -  end -    def itemized?      @itemize    end -  def paginated? -    @paginator.present? -  end -    def represent(resource, opts = {})      if itemized?        itemize(resource).map do |item| @@ -27,8 +21,6 @@ class EnvironmentSerializer < BaseSerializer            latest: super(item.latest, opts) }        end      else -      resource = @paginator.paginate(resource) if paginated? -        super(resource, opts)      end    end diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb new file mode 100644 index 00000000000..37240bfb0b1 --- /dev/null +++ b/app/serializers/group_child_entity.rb @@ -0,0 +1,77 @@ +class GroupChildEntity < Grape::Entity +  include ActionView::Helpers::NumberHelper +  include RequestAwareEntity + +  expose :id, :name, :description, :visibility, :full_name, +         :created_at, :updated_at, :avatar_url + +  expose :type do |instance| +    type +  end + +  expose :can_edit do |instance| +    return false unless request.respond_to?(:current_user) + +    can?(request.current_user, "admin_#{type}", instance) +  end + +  expose :edit_path do |instance| +    # We know `type` will be one either `project` or `group`. +    # The `edit_polymorphic_path` helper would try to call the path helper +    # with a plural: `edit_groups_path(instance)` or `edit_projects_path(instance)` +    # while our methods are `edit_group_path` or `edit_group_path` +    public_send("edit_#{type}_path", instance) # rubocop:disable GitlabSecurity/PublicSend +  end + +  expose :relative_path do |instance| +    polymorphic_path(instance) +  end + +  expose :permission do |instance| +    membership&.human_access +  end + +  # Project only attributes +  expose :star_count, +         if: lambda { |_instance, _options| project? } + +  # Group only attributes +  expose :children_count, :parent_id, :project_count, :subgroup_count, +         unless: lambda { |_instance, _options| project? } + +  expose :leave_path, unless: lambda { |_instance, _options| project? } do |instance| +    leave_group_members_path(instance) +  end + +  expose :can_leave, unless: lambda { |_instance, _options| project? } do |instance| +    if membership +      can?(request.current_user, :destroy_group_member, membership) +    else +      false +    end +  end + +  expose :number_projects_with_delimiter, unless: lambda { |_instance, _options| project? } do |instance| +    number_with_delimiter(instance.project_count) +  end + +  expose :number_users_with_delimiter, unless: lambda { |_instance, _options| project? } do |instance| +    number_with_delimiter(instance.member_count) +  end + +  private + +  def membership +    return unless request.current_user + +    @membership ||= request.current_user.members.find_by(source: object) +  end + +  def project? +    object.is_a?(Project) +  end + +  def type +    object.class.name.downcase +  end +end diff --git a/app/serializers/group_child_serializer.rb b/app/serializers/group_child_serializer.rb new file mode 100644 index 00000000000..2baef0a5703 --- /dev/null +++ b/app/serializers/group_child_serializer.rb @@ -0,0 +1,51 @@ +class GroupChildSerializer < BaseSerializer +  include WithPagination + +  attr_reader :hierarchy_root, :should_expand_hierarchy + +  entity GroupChildEntity + +  def expand_hierarchy(hierarchy_root = nil) +    @hierarchy_root = hierarchy_root +    @should_expand_hierarchy = true + +    self +  end + +  def represent(resource, opts = {}, entity_class = nil) +    if should_expand_hierarchy +      paginator.paginate(resource) if paginated? +      represent_hierarchies(resource, opts) +    else +      super(resource, opts) +    end +  end + +  protected + +  def represent_hierarchies(children, opts) +    if children.is_a?(GroupDescendant) +      represent_hierarchy(children.hierarchy(hierarchy_root), opts).first +    else +      hierarchies = GroupDescendant.build_hierarchy(children, hierarchy_root) +      # When an array was passed, we always want to represent an array. +      # Even if the hierarchy only contains one element +      represent_hierarchy(Array.wrap(hierarchies), opts) +    end +  end + +  def represent_hierarchy(hierarchy, opts) +    serializer = self.class.new(params) + +    if hierarchy.is_a?(Hash) +      hierarchy.map do |parent, children| +        serializer.represent(parent, opts) +          .merge(children: Array.wrap(serializer.represent_hierarchy(children, opts))) +      end +    elsif hierarchy.is_a?(Array) +      hierarchy.flat_map { |child| serializer.represent_hierarchy(child, opts) } +    else +      serializer.represent(hierarchy, opts) +    end +  end +end diff --git a/app/serializers/group_entity.rb b/app/serializers/group_entity.rb index 7c872a3e986..6d8466da902 100644 --- a/app/serializers/group_entity.rb +++ b/app/serializers/group_entity.rb @@ -45,6 +45,6 @@ class GroupEntity < Grape::Entity    end    expose :avatar_url do |group| -    group_icon(group) +    group_icon_url(group)    end  end diff --git a/app/serializers/group_serializer.rb b/app/serializers/group_serializer.rb index 26e8566828b..8cf7eb63bcf 100644 --- a/app/serializers/group_serializer.rb +++ b/app/serializers/group_serializer.rb @@ -1,19 +1,5 @@  class GroupSerializer < BaseSerializer -  entity GroupEntity - -  def with_pagination(request, response) -    tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) } -  end +  include WithPagination -  def paginated? -    @paginator.present? -  end - -  def represent(resource, opts = {}) -    if paginated? -      super(@paginator.paginate(resource), opts) -    else -      super(resource, opts) -    end -  end +  entity GroupEntity  end diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb index 61c7a428745..3b5a4fd4f79 100644 --- a/app/serializers/issuable_entity.rb +++ b/app/serializers/issuable_entity.rb @@ -1,20 +1,16 @@  class IssuableEntity < Grape::Entity +  include RequestAwareEntity +    expose :id    expose :iid    expose :author_id    expose :description    expose :lock_version    expose :milestone_id -  expose :state    expose :title    expose :updated_by_id    expose :created_at    expose :updated_at -  expose :deleted_at -  expose :time_estimate -  expose :total_time_spent -  expose :human_time_estimate -  expose :human_total_time_spent    expose :milestone, using: API::Entities::Milestone    expose :labels, using: LabelEntity  end diff --git a/app/serializers/issuable_sidebar_entity.rb b/app/serializers/issuable_sidebar_entity.rb new file mode 100644 index 00000000000..ff23d8bf0c7 --- /dev/null +++ b/app/serializers/issuable_sidebar_entity.rb @@ -0,0 +1,16 @@ +class IssuableSidebarEntity < Grape::Entity +  include RequestAwareEntity + +  expose :participants, using: ::API::Entities::UserBasic do |issuable| +    issuable.participants(request.current_user) +  end + +  expose :subscribed do |issuable| +    issuable.subscribed?(request.current_user, issuable.project) +  end + +  expose :time_estimate +  expose :total_time_spent +  expose :human_time_estimate +  expose :human_total_time_spent +end diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index 10d3ad0214b..5f47592e4ad 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -1,6 +1,8 @@  class IssueEntity < IssuableEntity -  include RequestAwareEntity +  include TimeTrackableEntity +  expose :state +  expose :deleted_at    expose :branch_name    expose :confidential    expose :discussion_locked diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb index 4fff54a9126..2555595379b 100644 --- a/app/serializers/issue_serializer.rb +++ b/app/serializers/issue_serializer.rb @@ -1,3 +1,16 @@  class IssueSerializer < BaseSerializer -  entity IssueEntity +  # This overrided method takes care of which entity should be used +  # to serialize the `issue` based on `basic` key in `opts` param. +  # Hence, `entity` doesn't need to be declared on the class scope. +  def represent(merge_request, opts = {}) +    entity = +      case opts[:serializer] +      when 'sidebar' +        IssueSidebarEntity +      else +        IssueEntity +      end + +    super(merge_request, opts, entity) +  end  end diff --git a/app/serializers/issue_sidebar_entity.rb b/app/serializers/issue_sidebar_entity.rb new file mode 100644 index 00000000000..6c823dbfe95 --- /dev/null +++ b/app/serializers/issue_sidebar_entity.rb @@ -0,0 +1,3 @@ +class IssueSidebarEntity < IssuableSidebarEntity +  expose :assignees, using: API::Entities::UserBasic +end diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb index 8461f158bb5..d54a6516aed 100644 --- a/app/serializers/merge_request_basic_entity.rb +++ b/app/serializers/merge_request_basic_entity.rb @@ -1,11 +1,7 @@ -class MergeRequestBasicEntity < Grape::Entity +class MergeRequestBasicEntity < IssuableSidebarEntity    expose :assignee_id    expose :merge_status    expose :merge_error    expose :state    expose :source_branch_exists?, as: :source_branch_exists -  expose :time_estimate -  expose :total_time_spent -  expose :human_time_estimate -  expose :human_total_time_spent  end diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb index 297a459e394..b53a49fe59e 100644 --- a/app/serializers/merge_request_entity.rb +++ b/app/serializers/merge_request_entity.rb @@ -1,6 +1,8 @@  class MergeRequestEntity < IssuableEntity -  include RequestAwareEntity +  include TimeTrackableEntity +  expose :state +  expose :deleted_at    expose :in_progress_merge_commit_sha    expose :merge_commit_sha    expose :merge_error diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb index f67034ce47a..e9d98d8baca 100644 --- a/app/serializers/merge_request_serializer.rb +++ b/app/serializers/merge_request_serializer.rb @@ -3,7 +3,14 @@ class MergeRequestSerializer < BaseSerializer    # to serialize the `merge_request` based on `basic` key in `opts` param.    # Hence, `entity` doesn't need to be declared on the class scope.    def represent(merge_request, opts = {}) -    entity = opts[:basic] ? MergeRequestBasicEntity : MergeRequestEntity +    entity = +      case opts[:serializer] +      when 'basic', 'sidebar' +        MergeRequestBasicEntity +      else +        MergeRequestEntity +      end +      super(merge_request, opts, entity)    end  end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index 661bf17983c..7181f8a6b04 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -1,16 +1,10 @@  class PipelineSerializer < BaseSerializer +  include WithPagination +    InvalidResourceError = Class.new(StandardError)    entity PipelineDetailsEntity -  def with_pagination(request, response) -    tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) } -  end - -  def paginated? -    @paginator.present? -  end -    def represent(resource, opts = {})      if resource.is_a?(ActiveRecord::Relation) diff --git a/app/serializers/submodule_entity.rb b/app/serializers/submodule_entity.rb index 9a7eb5e7880..ed1f1ae0ef0 100644 --- a/app/serializers/submodule_entity.rb +++ b/app/serializers/submodule_entity.rb @@ -7,7 +7,7 @@ class SubmoduleEntity < Grape::Entity      'archive'    end -  expose :project_url do |blob| +  expose :url do |blob|      submodule_links(blob, request).first    end diff --git a/app/serializers/time_trackable_entity.rb b/app/serializers/time_trackable_entity.rb new file mode 100644 index 00000000000..e81cd7bec72 --- /dev/null +++ b/app/serializers/time_trackable_entity.rb @@ -0,0 +1,11 @@ +module TimeTrackableEntity +  extend ActiveSupport::Concern +  extend Grape + +  included do +    expose :time_estimate +    expose :total_time_spent +    expose :human_time_estimate +    expose :human_total_time_spent +  end +end diff --git a/app/serializers/tree_entity.rb b/app/serializers/tree_entity.rb index 555e5cf83bd..9f1b485347f 100644 --- a/app/serializers/tree_entity.rb +++ b/app/serializers/tree_entity.rb @@ -3,10 +3,6 @@ class TreeEntity < Grape::Entity    expose :id, :path, :name, :mode -  expose :last_commit do |tree| -    request.project.repository.last_commit_for_path(tree.commit_id, tree.path) -  end -    expose :icon do |tree|      IconsHelper.file_type_icon_class('folder', tree.mode, tree.name)    end diff --git a/app/serializers/tree_root_entity.rb b/app/serializers/tree_root_entity.rb index 69702ae1493..496f070ddbd 100644 --- a/app/serializers/tree_root_entity.rb +++ b/app/serializers/tree_root_entity.rb @@ -18,4 +18,8 @@ class TreeRootEntity < Grape::Entity      project_tree_path(request.project, File.join(request.ref, parent_tree_path))    end + +  expose :last_commit_path do |tree| +    logs_file_project_ref_path(request.project, request.ref, tree.path) +  end  end diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index 9c00ea789ec..46e19230328 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -39,11 +39,8 @@ class AccessTokenValidationService        token_scopes = token.scopes.map(&:to_sym)        required_scopes.any? do |scope| -        if scope.respond_to?(:sufficient?) -          scope.sufficient?(token_scopes, request) -        else -          API::Scope.new(scope).sufficient?(token_scopes, request) -        end +        scope = API::Scope.new(scope) unless scope.is_a?(API::Scope) +        scope.sufficient?(token_scopes, request)        end      end    end diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb new file mode 100644 index 00000000000..35d45f25a71 --- /dev/null +++ b/app/services/applications/create_service.rb @@ -0,0 +1,13 @@ +module Applications +  class CreateService +    def initialize(current_user, params) +      @current_user = current_user +      @params = params +      @ip_address = @params.delete(:ip_address) +    end + +    def execute(request = nil) +      Doorkeeper::Application.create(@params) +    end +  end +end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 9a636346899..f40cd2b06c8 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -56,11 +56,22 @@ module Auth      def process_scope(scope)        type, name, actions = scope.split(':', 3)        actions = actions.split(',') -      path = ContainerRegistry::Path.new(name) -      return unless type == 'repository' +      case type +      when 'registry' +        process_registry_access(type, name, actions) +      when 'repository' +        path = ContainerRegistry::Path.new(name) +        process_repository_access(type, path, actions) +      end +    end + +    def process_registry_access(type, name, actions) +      return unless current_user&.admin? +      return unless name == 'catalog' +      return unless actions == ['*'] -      process_repository_access(type, path, actions) +      { type: type, name: name, actions: ['*'] }      end      def process_repository_access(type, path, actions) diff --git a/app/services/base_renderer.rb b/app/services/base_renderer.rb new file mode 100644 index 00000000000..d6e30bd7008 --- /dev/null +++ b/app/services/base_renderer.rb @@ -0,0 +1,7 @@ +class BaseRenderer +  attr_reader :current_user + +  def initialize(current_user = nil) +    @current_user = current_user +  end +end diff --git a/app/services/ci/create_cluster_service.rb b/app/services/ci/create_cluster_service.rb deleted file mode 100644 index f7ee0e468e2..00000000000 --- a/app/services/ci/create_cluster_service.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Ci -  class CreateClusterService < BaseService -    def execute(access_token) -      params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE - -      cluster_params = -        params.merge(user: current_user, -                     gcp_token: access_token) - -      project.create_cluster(cluster_params).tap do |cluster| -        ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? -      end -    end -  end -end diff --git a/app/services/ci/ensure_stage_service.rb b/app/services/ci/ensure_stage_service.rb new file mode 100644 index 00000000000..dc2f49e8db1 --- /dev/null +++ b/app/services/ci/ensure_stage_service.rb @@ -0,0 +1,39 @@ +module Ci +  ## +  # We call this service everytime we persist a CI/CD job. +  # +  # In most cases a job should already have a stage assigned,  but in cases it +  # doesn't have we need to either find existing one or create a brand new +  # stage. +  # +  class EnsureStageService < BaseService +    def execute(build) +      @build = build + +      return if build.stage_id.present? +      return if build.invalid? + +      ensure_stage.tap do |stage| +        build.stage_id = stage.id + +        yield stage if block_given? +      end +    end + +    private + +    def ensure_stage +      find_stage || create_stage +    end + +    def find_stage +      @build.pipeline.stages.find_by(name: @build.stage) +    end + +    def create_stage +      Ci::Stage.create!(name: @build.stage, +                        pipeline: @build.pipeline, +                        project: @build.project) +    end +  end +end diff --git a/app/services/ci/fetch_gcp_operation_service.rb b/app/services/ci/fetch_gcp_operation_service.rb deleted file mode 100644 index 0b68e4d6ea9..00000000000 --- a/app/services/ci/fetch_gcp_operation_service.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Ci -  class FetchGcpOperationService -    def execute(cluster) -      api_client = -        GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil) - -      operation = api_client.projects_zones_operations( -        cluster.gcp_project_id, -        cluster.gcp_cluster_zone, -        cluster.gcp_operation_id) - -      yield(operation) if block_given? -    rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e -      return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") -    end -  end -end diff --git a/app/services/ci/finalize_cluster_creation_service.rb b/app/services/ci/finalize_cluster_creation_service.rb deleted file mode 100644 index 347875c5697..00000000000 --- a/app/services/ci/finalize_cluster_creation_service.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Ci -  class FinalizeClusterCreationService -    def execute(cluster) -      api_client = -        GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil) - -      begin -        gke_cluster = api_client.projects_zones_clusters_get( -          cluster.gcp_project_id, -          cluster.gcp_cluster_zone, -          cluster.gcp_cluster_name) -      rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e -        return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") -      end - -      endpoint = gke_cluster.endpoint -      api_url = 'https://' + endpoint -      ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate) -      username = gke_cluster.master_auth.username -      password = gke_cluster.master_auth.password - -      kubernetes_token = Ci::FetchKubernetesTokenService.new( -        api_url, ca_cert, username, password).execute - -      unless kubernetes_token -        return cluster.make_errored!('Failed to get a default token of kubernetes') -      end - -      Ci::IntegrateClusterService.new.execute( -        cluster, endpoint, ca_cert, kubernetes_token, username, password) -    end -  end -end diff --git a/app/services/ci/integrate_cluster_service.rb b/app/services/ci/integrate_cluster_service.rb deleted file mode 100644 index d123ce8d26b..00000000000 --- a/app/services/ci/integrate_cluster_service.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Ci -  class IntegrateClusterService -    def execute(cluster, endpoint, ca_cert, token, username, password) -      Gcp::Cluster.transaction do -        cluster.update!( -          enabled: true, -          endpoint: endpoint, -          ca_cert: ca_cert, -          kubernetes_token: token, -          username: username, -          password: password, -          service: cluster.project.find_or_initialize_service('kubernetes'), -          status_event: :make_created) - -        cluster.service.update!( -          active: true, -          api_url: cluster.api_url, -          ca_pem: ca_cert, -          namespace: cluster.project_namespace, -          token: token) -      end -    rescue ActiveRecord::RecordInvalid => e -      cluster.make_errored!("Failed to integrate cluster into kubernetes_service: #{e.message}") -    end -  end -end diff --git a/app/services/ci/provision_cluster_service.rb b/app/services/ci/provision_cluster_service.rb deleted file mode 100644 index 52d80b01813..00000000000 --- a/app/services/ci/provision_cluster_service.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Ci -  class ProvisionClusterService -    def execute(cluster) -      api_client = -        GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil) - -      begin -        operation = api_client.projects_zones_clusters_create( -          cluster.gcp_project_id, -          cluster.gcp_cluster_zone, -          cluster.gcp_cluster_name, -          cluster.gcp_cluster_size, -          machine_type: cluster.gcp_machine_type) -      rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e -        return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") -      end - -      unless operation.status == 'RUNNING' || operation.status == 'PENDING' -        return cluster.make_errored!("Operation status is unexpected; #{operation.status_message}") -      end - -      cluster.gcp_operation_id = api_client.parse_operation_id(operation.self_link) - -      unless cluster.gcp_operation_id -        return cluster.make_errored!('Can not find operation_id from self_link') -      end - -      if cluster.make_creating -        WaitForClusterCreationWorker.perform_in( -          WaitForClusterCreationWorker::INITIAL_INTERVAL, cluster.id) -      else -        return cluster.make_errored!("Failed to update cluster record; #{cluster.errors}") -      end -    end -  end -end diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index d67b9f5cc56..c552193e66b 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -28,6 +28,8 @@ module Ci        attributes.push([:user, current_user]) +      build.retried = true +        Ci::Build.transaction do          # mark all other builds of that name as retried          build.pipeline.builds.latest diff --git a/app/services/ci/update_cluster_service.rb b/app/services/ci/update_cluster_service.rb deleted file mode 100644 index 70d88fca660..00000000000 --- a/app/services/ci/update_cluster_service.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Ci -  class UpdateClusterService < BaseService -    def execute(cluster) -      Gcp::Cluster.transaction do -        cluster.update!(params) - -        if params['enabled'] == 'true' -          cluster.service.update!( -            active: true, -            api_url: cluster.api_url, -            ca_pem: cluster.ca_cert, -            namespace: cluster.project_namespace, -            token: cluster.kubernetes_token) -        else -          cluster.service.update!(active: false) -        end -      end -    rescue ActiveRecord::RecordInvalid => e -      cluster.errors.add(:base, e.message) -    end -  end -end diff --git a/app/services/clusters/applications/base_helm_service.rb b/app/services/clusters/applications/base_helm_service.rb new file mode 100644 index 00000000000..9a4ce31cb39 --- /dev/null +++ b/app/services/clusters/applications/base_helm_service.rb @@ -0,0 +1,29 @@ +module Clusters +  module Applications +    class BaseHelmService +      attr_accessor :app + +      def initialize(app) +        @app = app +      end + +      protected + +      def cluster +        app.cluster +      end + +      def kubeclient +        cluster.kubeclient +      end + +      def helm_api +        @helm_api ||= Gitlab::Kubernetes::Helm.new(kubeclient) +      end + +      def install_command +        @install_command ||= app.install_command +      end +    end +  end +end diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb new file mode 100644 index 00000000000..bde090eaeec --- /dev/null +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -0,0 +1,65 @@ +module Clusters +  module Applications +    class CheckInstallationProgressService < BaseHelmService +      def execute +        return unless app.installing? + +        case installation_phase +        when Gitlab::Kubernetes::Pod::SUCCEEDED +          on_success +        when Gitlab::Kubernetes::Pod::FAILED +          on_failed +        else +          check_timeout +        end +      rescue KubeException => ke +        app.make_errored!("Kubernetes error: #{ke.message}") unless app.errored? +      end + +      private + +      def on_success +        app.make_installed! +      ensure +        remove_installation_pod +      end + +      def on_failed +        app.make_errored!(installation_errors || 'Installation silently failed') +      ensure +        remove_installation_pod +      end + +      def check_timeout +        if timeouted? +          begin +            app.make_errored!('Installation timeouted') +          ensure +            remove_installation_pod +          end +        else +          ClusterWaitForAppInstallationWorker.perform_in( +            ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) +        end +      end + +      def timeouted? +        Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT +      end + +      def remove_installation_pod +        helm_api.delete_installation_pod!(install_command.pod_name) +      rescue +        # no-op +      end + +      def installation_phase +        helm_api.installation_status(install_command.pod_name) +      end + +      def installation_errors +        helm_api.installation_log(install_command.pod_name) +      end +    end +  end +end diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb new file mode 100644 index 00000000000..8ceeec687cd --- /dev/null +++ b/app/services/clusters/applications/install_service.rb @@ -0,0 +1,21 @@ +module Clusters +  module Applications +    class InstallService < BaseHelmService +      def execute +        return unless app.scheduled? + +        begin +          app.make_installing! +          helm_api.install(install_command) + +          ClusterWaitForAppInstallationWorker.perform_in( +            ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) +        rescue KubeException => ke +          app.make_errored!("Kubernetes error: #{ke.message}") +        rescue StandardError +          app.make_errored!("Can't start installation process") +        end +      end +    end +  end +end diff --git a/app/services/clusters/applications/schedule_installation_service.rb b/app/services/clusters/applications/schedule_installation_service.rb new file mode 100644 index 00000000000..eb8caa68ef7 --- /dev/null +++ b/app/services/clusters/applications/schedule_installation_service.rb @@ -0,0 +1,22 @@ +module Clusters +  module Applications +    class ScheduleInstallationService < ::BaseService +      def execute +        application_class.find_or_create_by!(cluster: cluster).try do |application| +          application.make_scheduled! +          ClusterInstallAppWorker.perform_async(application.name, application.id) +        end +      end + +      private + +      def application_class +        params[:application_class] +      end + +      def cluster +        params[:cluster] +      end +    end +  end +end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb new file mode 100644 index 00000000000..1d407739b21 --- /dev/null +++ b/app/services/clusters/create_service.rb @@ -0,0 +1,29 @@ +module Clusters +  class CreateService < BaseService +    attr_reader :access_token + +    def execute(access_token) +      @access_token = access_token + +      create_cluster.tap do |cluster| +        ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? +      end +    end + +    private + +    def create_cluster +      Clusters::Cluster.create(cluster_params) +    end + +    def cluster_params +      return @cluster_params if defined?(@cluster_params) + +      params[:provider_gcp_attributes].try do |provider| +        provider[:access_token] = access_token +      end + +      @cluster_params = params.merge(user: current_user, projects: [project]) +    end +  end +end diff --git a/app/services/clusters/gcp/fetch_operation_service.rb b/app/services/clusters/gcp/fetch_operation_service.rb new file mode 100644 index 00000000000..a4cd3ca5c11 --- /dev/null +++ b/app/services/clusters/gcp/fetch_operation_service.rb @@ -0,0 +1,16 @@ +module Clusters +  module Gcp +    class FetchOperationService +      def execute(provider) +        operation = provider.api_client.projects_zones_operations( +          provider.gcp_project_id, +          provider.zone, +          provider.operation_id) + +        yield(operation) if block_given? +      rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e +        provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") +      end +    end +  end +end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb new file mode 100644 index 00000000000..cea56f4e849 --- /dev/null +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -0,0 +1,56 @@ +module Clusters +  module Gcp +    class FinalizeCreationService +      attr_reader :provider + +      def execute(provider) +        @provider = provider + +        configure_provider +        configure_kubernetes + +        cluster.save! +      rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e +        provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") +      rescue ActiveRecord::RecordInvalid => e +        provider.make_errored!("Failed to configure GKE Cluster: #{e.message}") +      end + +      private + +      def configure_provider +        provider.endpoint = gke_cluster.endpoint +        provider.status_event = :make_created +      end + +      def configure_kubernetes +        cluster.platform_type = :kubernetes +        cluster.build_platform_kubernetes( +          api_url: 'https://' + gke_cluster.endpoint, +          ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), +          username: gke_cluster.master_auth.username, +          password: gke_cluster.master_auth.password, +          token: request_kuberenetes_token) +      end + +      def request_kuberenetes_token +        Ci::FetchKubernetesTokenService.new( +          'https://' + gke_cluster.endpoint, +          Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), +          gke_cluster.master_auth.username, +          gke_cluster.master_auth.password).execute +      end + +      def gke_cluster +        @gke_cluster ||= provider.api_client.projects_zones_clusters_get( +          provider.gcp_project_id, +          provider.zone, +          cluster.name) +      end + +      def cluster +        @cluster ||= provider.cluster +      end +    end +  end +end diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb new file mode 100644 index 00000000000..8beea5a8cfb --- /dev/null +++ b/app/services/clusters/gcp/provision_service.rb @@ -0,0 +1,47 @@ +module Clusters +  module Gcp +    class ProvisionService +      attr_reader :provider + +      def execute(provider) +        @provider = provider + +        get_operation_id do |operation_id| +          if provider.make_creating(operation_id) +            WaitForClusterCreationWorker.perform_in( +              Clusters::Gcp::VerifyProvisionStatusService::INITIAL_INTERVAL, +              provider.cluster_id) +          else +            provider.make_errored!("Failed to update provider record; #{provider.errors}") +          end +        end +      end + +      private + +      def get_operation_id +        operation = provider.api_client.projects_zones_clusters_create( +          provider.gcp_project_id, +          provider.zone, +          provider.cluster.name, +          provider.num_nodes, +          machine_type: provider.machine_type) + +        unless operation.status == 'PENDING' || operation.status == 'RUNNING' +          return provider.make_errored!("Operation status is unexpected; #{operation.status_message}") +        end + +        operation_id = provider.api_client.parse_operation_id(operation.self_link) + +        unless operation_id +          return provider.make_errored!('Can not find operation_id from self_link') +        end + +        yield(operation_id) + +      rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e +        provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") +      end +    end +  end +end diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb new file mode 100644 index 00000000000..bc33756f27c --- /dev/null +++ b/app/services/clusters/gcp/verify_provision_status_service.rb @@ -0,0 +1,48 @@ +module Clusters +  module Gcp +    class VerifyProvisionStatusService +      attr_reader :provider + +      INITIAL_INTERVAL = 2.minutes +      EAGER_INTERVAL = 10.seconds +      TIMEOUT = 20.minutes + +      def execute(provider) +        @provider = provider + +        request_operation do |operation| +          case operation.status +          when 'PENDING', 'RUNNING' +            continue_creation(operation) +          when 'DONE' +            finalize_creation +          else +            return provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}") +          end +        end +      end + +      private + +      def continue_creation(operation) +        if elapsed_time_from_creation(operation) < TIMEOUT +          WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id) +        else +          provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}") +        end +      end + +      def elapsed_time_from_creation(operation) +        Time.now.utc - operation.start_time.to_time.utc +      end + +      def finalize_creation +        Clusters::Gcp::FinalizeCreationService.new.execute(provider) +      end + +      def request_operation(&blk) +        Clusters::Gcp::FetchOperationService.new.execute(provider, &blk) +      end +    end +  end +end diff --git a/app/services/clusters/update_service.rb b/app/services/clusters/update_service.rb new file mode 100644 index 00000000000..989218e32a2 --- /dev/null +++ b/app/services/clusters/update_service.rb @@ -0,0 +1,7 @@ +module Clusters +  class UpdateService < BaseService +    def execute(cluster) +      cluster.update(params) +    end +  end +end diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb index 077268b2388..cb235a85daf 100644 --- a/app/services/delete_merged_branches_service.rb +++ b/app/services/delete_merged_branches_service.rb @@ -13,7 +13,7 @@ class DeleteMergedBranchesService < BaseService        # Prevent deletion of branches relevant to open merge requests        branches -= merge_request_branch_names        # Prevent deletion of protected branches -      branches = branches.reject { |branch| project.protected_for?(branch) } +      branches = branches.reject { |branch| ProtectedBranch.protected?(project, branch) }        branches.each do |branch|          DeleteBranchService.new(project, current_user).execute(branch) diff --git a/app/services/events/render_service.rb b/app/services/events/render_service.rb new file mode 100644 index 00000000000..0b62d8aedf1 --- /dev/null +++ b/app/services/events/render_service.rb @@ -0,0 +1,21 @@ +module Events +  class RenderService < BaseRenderer +    def execute(events, atom_request: false) +      events.map(&:note).compact.group_by(&:project).each do |project, notes| +        render_notes(notes, project, atom_request) +      end +    end + +    private + +    def render_notes(notes, project, atom_request) +      Notes::RenderService.new(current_user).execute(notes, project, render_options(atom_request)) +    end + +    def render_options(atom_request) +      return {} unless atom_request + +      { only_path: false, xhtml: true } +    end +  end +end diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb new file mode 100644 index 00000000000..92eaa5d5115 --- /dev/null +++ b/app/services/issuable/common_system_notes_service.rb @@ -0,0 +1,81 @@ +module Issuable +  class CommonSystemNotesService < ::BaseService +    attr_reader :issuable + +    def execute(issuable, old_labels) +      @issuable = issuable + +      if issuable.previous_changes.include?('title') +        create_title_change_note(issuable.previous_changes['title'].first) +      end + +      handle_description_change_note + +      handle_time_tracking_note if issuable.is_a?(TimeTrackable) +      create_labels_note(old_labels) if issuable.labels != old_labels +      create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked') +      create_milestone_note if issuable.previous_changes.include?('milestone_id') +    end + +    private + +    def handle_time_tracking_note +      if issuable.previous_changes.include?('time_estimate') +        create_time_estimate_note +      end + +      if issuable.time_spent? +        create_time_spent_note +      end +    end + +    def handle_description_change_note +      if issuable.previous_changes.include?('description') +        if issuable.tasks? && issuable.updated_tasks.any? +          create_task_status_note +        else +          # TODO: Show this note if non-task content was modified. +          # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577 +          create_description_change_note +        end +      end +    end + +    def create_labels_note(old_labels) +      added_labels = issuable.labels - old_labels +      removed_labels = old_labels - issuable.labels + +      SystemNoteService.change_label(issuable, issuable.project, current_user, added_labels, removed_labels) +    end + +    def create_title_change_note(old_title) +      SystemNoteService.change_title(issuable, issuable.project, current_user, old_title) +    end + +    def create_description_change_note +      SystemNoteService.change_description(issuable, issuable.project, current_user) +    end + +    def create_task_status_note +      issuable.updated_tasks.each do |task| +        SystemNoteService.change_task_status(issuable, issuable.project, current_user, task) +      end +    end + +    def create_time_estimate_note +      SystemNoteService.change_time_estimate(issuable, issuable.project, current_user) +    end + +    def create_time_spent_note +      SystemNoteService.change_time_spent(issuable, issuable.project, issuable.time_spent_user) +    end + +    def create_milestone_note +      SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone) +    end + +    def create_discussion_lock_note +      SystemNoteService.discussion_lock(issuable, current_user) +    end +  end +end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index f83ece7098f..68b49d880f7 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -1,56 +1,10 @@  class IssuableBaseService < BaseService    private -  def create_milestone_note(issuable) -    SystemNoteService.change_milestone( -      issuable, issuable.project, current_user, issuable.milestone) -  end - -  def create_labels_note(issuable, old_labels) -    added_labels = issuable.labels - old_labels -    removed_labels = old_labels - issuable.labels - -    SystemNoteService.change_label( -      issuable, issuable.project, current_user, added_labels, removed_labels) -  end - -  def create_title_change_note(issuable, old_title) -    SystemNoteService.change_title( -      issuable, issuable.project, current_user, old_title) -  end - -  def create_description_change_note(issuable) -    SystemNoteService.change_description(issuable, issuable.project, current_user) -  end - -  def create_branch_change_note(issuable, branch_type, old_branch, new_branch) -    SystemNoteService.change_branch( -      issuable, issuable.project, current_user, branch_type, -      old_branch, new_branch) -  end - -  def create_task_status_note(issuable) -    issuable.updated_tasks.each do |task| -      SystemNoteService.change_task_status(issuable, issuable.project, current_user, task) -    end -  end - -  def create_time_estimate_note(issuable) -    SystemNoteService.change_time_estimate(issuable, issuable.project, current_user) -  end - -  def create_time_spent_note(issuable) -    SystemNoteService.change_time_spent(issuable, issuable.project, current_user) -  end - -  def create_discussion_lock_note(issuable) -    SystemNoteService.discussion_lock(issuable, current_user) -  end -    def filter_params(issuable)      ability_name = :"admin_#{issuable.to_ability_name}" -    unless can?(current_user, ability_name, project) +    unless can?(current_user, ability_name, issuable)        params.delete(:milestone_id)        params.delete(:labels)        params.delete(:add_label_ids) @@ -233,15 +187,14 @@ class IssuableBaseService < BaseService        # We have to perform this check before saving the issuable as Rails resets        # the changed fields upon calling #save. -      update_project_counters = issuable.update_project_counter_caches? +      update_project_counters = issuable.project && issuable.update_project_counter_caches?        if issuable.with_transaction_returning_status { issuable.save }          # We do not touch as it will affect a update on updated_at field          ActiveRecord::Base.no_touching do -          handle_common_system_notes(issuable, old_labels: old_labels) +          Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels)          end -        change_discussion_lock(issuable)          handle_changes(            issuable,            old_labels: old_labels, @@ -255,7 +208,7 @@ class IssuableBaseService < BaseService          invalidate_cache_counts(issuable, users: affected_assignees.compact)          after_update(issuable)          issuable.create_new_cross_references!(current_user) -        execute_hooks(issuable, 'update') +        execute_hooks(issuable, 'update', old_labels: old_labels, old_assignees: old_assignees)          issuable.update_project_counter_caches if update_project_counters        end @@ -300,12 +253,6 @@ class IssuableBaseService < BaseService      end    end -  def change_discussion_lock(issuable) -    if issuable.previous_changes.include?('discussion_locked') -      create_discussion_lock_note(issuable) -    end -  end -    def toggle_award(issuable)      award = params.delete(:emoji_award)      if award @@ -328,35 +275,17 @@ class IssuableBaseService < BaseService      attrs_changed || labels_changed || assignees_changed    end -  def handle_common_system_notes(issuable, old_labels: []) -    if issuable.previous_changes.include?('title') -      create_title_change_note(issuable, issuable.previous_changes['title'].first) -    end - -    if issuable.previous_changes.include?('description') -      if issuable.tasks? && issuable.updated_tasks.any? -        create_task_status_note(issuable) -      else -        # TODO: Show this note if non-task content was modified. -        # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577 -        create_description_change_note(issuable) -      end -    end - -    if issuable.previous_changes.include?('time_estimate') -      create_time_estimate_note(issuable) -    end - -    if issuable.time_spent? -      create_time_spent_note(issuable) -    end - -    create_labels_note(issuable, old_labels) if issuable.labels != old_labels -  end -    def invalidate_cache_counts(issuable, users: [])      users.each do |user|        user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend      end    end + +  # override if needed +  def handle_changes(issuable, options) +  end + +  # override if needed +  def execute_hooks(issuable, action = 'open', params = {}) +  end  end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 4c198fc96ea..735257c4779 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -1,10 +1,10 @@  module Issues    class BaseService < ::IssuableBaseService -    def hook_data(issue, action) -      issue_data = issue.to_hook_data(current_user) -      issue_url = Gitlab::UrlBuilder.build(issue) -      issue_data[:object_attributes].merge!(url: issue_url, action: action) -      issue_data +    def hook_data(issue, action, old_labels: [], old_assignees: []) +      hook_data = issue.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees) +      hook_data[:object_attributes][:action] = action + +      hook_data      end      def reopen_service @@ -22,8 +22,8 @@ module Issues          issue, issue.project, current_user, old_assignees)      end -    def execute_hooks(issue, action = 'open') -      issue_data  = hook_data(issue, action) +    def execute_hooks(issue, action = 'open', old_labels: [], old_assignees: []) +      issue_data  = hook_data(issue, action, old_labels: old_labels, old_assignees: old_assignees)        hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks        issue.project.execute_hooks(issue_data, hooks_scope)        issue.project.execute_services(issue_data, hooks_scope) diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb index 35de4337b15..62b4b4b6a1e 100644 --- a/app/services/issues/reopen_service.rb +++ b/app/services/issues/reopen_service.rb @@ -9,6 +9,7 @@ module Issues          notification_service.reopen_issue(issue, current_user)          execute_hooks(issue, 'reopen')          invalidate_cache_counts(issue, users: issue.assignees) +        issue.update_project_counter_caches        end        issue diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index e0339ddf9bb..1b7b5927c5a 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -27,10 +27,6 @@ module Issues          todo_service.update_issue(issue, current_user, old_mentioned_users)        end -      if issue.previous_changes.include?('milestone_id') -        create_milestone_note(issue) -      end -        if issue.assignees != old_assignees          create_assignee_note(issue, old_assignees)          notification_service.reassigned_issue(issue, current_user, old_assignees) diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb index 545832d0bd4..f78791932a7 100644 --- a/app/services/keys/base_service.rb +++ b/app/services/keys/base_service.rb @@ -4,6 +4,7 @@ module Keys      def initialize(user, params)        @user, @params = user, params +      @ip_address = @params.delete(:ip_address)      end      def notification_service diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb index 727768b1a39..6805b2f7d1c 100644 --- a/app/services/merge_requests/add_todo_when_build_fails_service.rb +++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb @@ -3,7 +3,7 @@ module MergeRequests      # Adds a todo to the parent merge_request when a CI build fails      #      def execute(commit_status) -      return if commit_status.allow_failure? +      return if commit_status.allow_failure? || commit_status.retried?        commit_status_merge_requests(commit_status) do |merge_request|          todo_service.merge_request_build_failed(merge_request) diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 35ccff26262..112606a82d7 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -18,19 +18,19 @@ module MergeRequests        super if changed_title      end -    def hook_data(merge_request, action, oldrev = nil) -      hook_data = merge_request.to_hook_data(current_user) -      hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request) +    def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: []) +      hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees)        hook_data[:object_attributes][:action] = action -      if oldrev && !Gitlab::Git.blank_ref?(oldrev) -        hook_data[:object_attributes][:oldrev] = oldrev +      if old_rev && !Gitlab::Git.blank_ref?(old_rev) +        hook_data[:object_attributes][:oldrev] = old_rev        end +        hook_data      end -    def execute_hooks(merge_request, action = 'open', oldrev = nil) +    def execute_hooks(merge_request, action = 'open', old_rev: nil, old_labels: [], old_assignees: [])        if merge_request.project -        merge_data = hook_data(merge_request, action, oldrev) +        merge_data = hook_data(merge_request, action, old_rev: old_rev, old_labels: old_labels, old_assignees: old_assignees)          merge_request.project.execute_hooks(merge_data, :merge_request_hooks)          merge_request.project.execute_services(merge_data, :merge_request_hooks)        end diff --git a/app/services/merge_requests/conflicts/list_service.rb b/app/services/merge_requests/conflicts/list_service.rb index 9835606812c..0f677a996f7 100644 --- a/app/services/merge_requests/conflicts/list_service.rb +++ b/app/services/merge_requests/conflicts/list_service.rb @@ -23,13 +23,13 @@ module MergeRequests            # when there are no conflict files.            conflicts.files.each(&:lines)            @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0 -        rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing +        rescue Rugged::OdbError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing            @conflicts_can_be_resolved_in_ui = false          end        end        def conflicts -        @conflicts ||= Gitlab::Conflict::FileCollection.read_only(merge_request) +        @conflicts ||= Gitlab::Conflict::FileCollection.new(merge_request)        end      end    end diff --git a/app/services/merge_requests/conflicts/resolve_service.rb b/app/services/merge_requests/conflicts/resolve_service.rb index 6b6e231f4f9..27cafd2d7d9 100644 --- a/app/services/merge_requests/conflicts/resolve_service.rb +++ b/app/services/merge_requests/conflicts/resolve_service.rb @@ -1,54 +1,10 @@  module MergeRequests    module Conflicts      class ResolveService < MergeRequests::Conflicts::BaseService -      MissingFiles = Class.new(Gitlab::Conflict::ResolutionError) -        def execute(current_user, params) -        rugged = merge_request.source_project.repository.rugged - -        Gitlab::Conflict::FileCollection.for_resolution(merge_request) do |conflicts_for_resolution| -          merge_index = conflicts_for_resolution.merge_index - -          params[:files].each do |file_params| -            conflict_file = conflicts_for_resolution.file_for_path(file_params[:old_path], file_params[:new_path]) - -            write_resolved_file_to_index(merge_index, rugged, conflict_file, file_params) -          end - -          unless merge_index.conflicts.empty? -            missing_files = merge_index.conflicts.map { |file| file[:ours][:path] } - -            raise MissingFiles, "Missing resolutions for the following files: #{missing_files.join(', ')}" -          end - -          commit_params = { -            message: params[:commit_message] || conflicts_for_resolution.default_commit_message, -            parents: [conflicts_for_resolution.our_commit, conflicts_for_resolution.their_commit].map(&:oid), -            tree: merge_index.write_tree(rugged) -          } - -          conflicts_for_resolution -            .project -            .repository -            .resolve_conflicts(current_user, merge_request.source_branch, commit_params) -        end -      end - -      private - -      def write_resolved_file_to_index(merge_index, rugged, file, params) -        if params[:sections] -          new_file = file.resolve_lines(params[:sections]).map(&:text).join("\n") - -          new_file << "\n" if file.our_blob.data.ends_with?("\n") -        elsif params[:content] -          new_file = file.resolve_content(params[:content]) -        end - -        our_path = file.our_path +        conflicts = Gitlab::Conflict::FileCollection.new(merge_request) -        merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode) -        merge_index.conflict_remove(our_path) +        conflicts.resolve(current_user, params[:commit_message], params[:files])        end      end    end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index a110abf8256..156e7b2f078 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -60,13 +60,9 @@ module MergeRequests      def after_merge        MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) -      if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch? -        # Verify again that the source branch can be removed, since branch may be protected, -        # or the source branch may have been updated. -        if @merge_request.can_remove_source_branch?(branch_deletion_user) -          DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) -            .execute(merge_request.source_branch) -        end +      if delete_source_branch? +        DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) +          .execute(merge_request.source_branch)        end      end @@ -78,16 +74,17 @@ module MergeRequests        @merge_request.force_remove_source_branch? ? @merge_request.author : current_user      end -    # Logs merge error message and cleans `MergeRequest#merge_jid`. +    # Verify again that the source branch can be removed, since branch may be protected, +    # or the source branch may have been updated, or the user may not have permission      # +    def delete_source_branch? +      params.fetch('should_remove_source_branch', @merge_request.force_remove_source_branch?) && +        @merge_request.can_remove_source_branch?(branch_deletion_user) +    end +      def handle_merge_error(log_message:, save_message_on_model: false)        Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}") - -      if save_message_on_model -        @merge_request.update(merge_error: log_message, merge_jid: nil) -      else -        clean_merge_jid -      end +      @merge_request.update(merge_error: log_message) if save_message_on_model      end      def merge_request_info diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index bc4a13cf4bc..fc100580c4f 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -166,7 +166,7 @@ module MergeRequests      # Call merge request webhook with update branches      def execute_mr_web_hooks        merge_requests_for_source_branch.each do |merge_request| -        execute_hooks(merge_request, 'update', @oldrev) +        execute_hooks(merge_request, 'update', old_rev: @oldrev)        end      end diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb index b9c65be36ec..c599a90f9fe 100644 --- a/app/services/merge_requests/reopen_service.rb +++ b/app/services/merge_requests/reopen_service.rb @@ -11,6 +11,7 @@ module MergeRequests          merge_request.reload_diff(current_user)          merge_request.mark_as_unchecked          invalidate_cache_counts(merge_request, users: merge_request.assignees) +        merge_request.update_project_counter_caches        end        merge_request diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 2832d893e95..1f394cacc64 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -40,10 +40,6 @@ module MergeRequests                                    merge_request.target_branch)        end -      if merge_request.previous_changes.include?('milestone_id') -        create_milestone_note(merge_request) -      end -        if merge_request.previous_changes.include?('assignee_id')          create_assignee_note(merge_request)          notification_service.reassigned_merge_request(merge_request, current_user) @@ -111,5 +107,11 @@ module MergeRequests                           end        end      end + +    def create_branch_change_note(issuable, branch_type, old_branch, new_branch) +      SystemNoteService.change_branch( +        issuable, issuable.project, current_user, branch_type, +        old_branch, new_branch) +    end    end  end diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb index a02eee4961b..6b3939aeba5 100644 --- a/app/services/metrics_service.rb +++ b/app/services/metrics_service.rb @@ -6,8 +6,7 @@ class MetricsService      Gitlab::HealthChecks::Redis::RedisCheck,      Gitlab::HealthChecks::Redis::CacheCheck,      Gitlab::HealthChecks::Redis::QueuesCheck, -    Gitlab::HealthChecks::Redis::SharedStateCheck, -    Gitlab::HealthChecks::FsShardsCheck +    Gitlab::HealthChecks::Redis::SharedStateCheck    ].freeze    def prometheus_metrics_text diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb new file mode 100644 index 00000000000..bd9cfd4e0ea --- /dev/null +++ b/app/services/milestones/promote_service.rb @@ -0,0 +1,80 @@ +module Milestones +  class PromoteService < Milestones::BaseService +    PromoteMilestoneError = Class.new(StandardError) + +    def execute(milestone) +      check_project_milestone!(milestone) + +      Milestone.transaction do +        # Destroy all milestones with same title across projects +        destroy_old_milestones(milestone) + +        group_milestone = clone_project_milestone(milestone) + +        move_children_to_group_milestone(group_milestone) + +        # Just to be safe +        unless group_milestone.valid? +          raise_error(group_milestone.errors.full_messages.to_sentence) +        end + +        group_milestone +      end +    end + +    private + +    def milestone_ids_for_merge(group_milestone) +      # Pluck need to be used here instead of select so the array of ids +      # is persistent after old milestones gets deleted. +      @milestone_ids_for_merge ||= begin +        search_params = { title: group_milestone.title, project_ids: group_project_ids, state: 'all' } +        milestones = MilestonesFinder.new(search_params).execute +        milestones.pluck(:id) +      end +    end + +    def move_children_to_group_milestone(group_milestone) +      milestone_ids_for_merge(group_milestone).in_groups_of(100) do |milestone_ids| +        update_children(group_milestone, milestone_ids) +      end +    end + +    def check_project_milestone!(milestone) +      raise_error('Only project milestones can be promoted.') unless milestone.project_milestone? +    end + +    def clone_project_milestone(milestone) +      params = milestone.slice(:title, :description, :start_date, :due_date, :state_event) + +      create_service = CreateService.new(group, current_user, params) + +      create_service.execute +    end + +    def update_children(group_milestone, milestone_ids) +      issues = Issue.where(project_id: group_project_ids, milestone_id: milestone_ids) +      merge_requests = MergeRequest.where(source_project_id: group_project_ids, milestone_id: milestone_ids) + +      [issues, merge_requests].each do |issuable_collection| +        issuable_collection.update_all(milestone_id: group_milestone.id) +      end +    end + +    def group +      @group ||= parent.group || raise_error('Project does not belong to a group.') +    end + +    def destroy_old_milestones(group_milestone) +      Milestone.where(id: milestone_ids_for_merge(group_milestone)).destroy_all +    end + +    def group_project_ids +      @group_project_ids ||= group.projects.map(&:id) +    end + +    def raise_error(message) +      raise PromoteMilestoneError, "Promotion failed - #{message}" +    end +  end +end diff --git a/app/services/notes/render_service.rb b/app/services/notes/render_service.rb new file mode 100644 index 00000000000..a77e98c2b07 --- /dev/null +++ b/app/services/notes/render_service.rb @@ -0,0 +1,21 @@ +module Notes +  class RenderService < BaseRenderer +    # Renders a collection of Note instances. +    # +    # notes - The notes to render. +    # project - The project to use for redacting. +    # user - The user viewing the notes. + +    # Possible options: +    # requested_path - The request path. +    # project_wiki - The project's wiki. +    # ref - The current Git reference. +    # only_path - flag to turn relative paths into absolute ones. +    # xhtml - flag to save the html in XHTML +    def execute(notes, project, **opts) +      renderer = Banzai::ObjectRenderer.new(project, current_user, **opts) + +      renderer.render(notes, :note) +    end +  end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 8d5da459882..be3b4b2ba07 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -390,7 +390,7 @@ class NotificationService    end    def relabeled_resource_email(target, labels, current_user, method) -    recipients = labels.flat_map { |l| l.subscribers(target.project) } +    recipients = labels.flat_map { |l| l.subscribers(target.project) }.uniq      recipients = notifiable_users(        recipients, :subscription,        target: target, diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 19d75ff2efa..81972df9b3c 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -51,7 +51,7 @@ module Projects      end      def wiki_path -      repo_path + '.wiki' +      project.wiki.disk_path      end      def trash_repositories! diff --git a/app/services/projects/group_links/create_service.rb b/app/services/projects/group_links/create_service.rb new file mode 100644 index 00000000000..35624577024 --- /dev/null +++ b/app/services/projects/group_links/create_service.rb @@ -0,0 +1,15 @@ +module Projects +  module GroupLinks +    class CreateService < BaseService +      def execute(group) +        return false unless group + +        project.project_group_links.create( +          group: group, +          group_access: params[:link_group_access], +          expires_at: params[:expires_at] +        ) +      end +    end +  end +end diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb new file mode 100644 index 00000000000..fbf31214c28 --- /dev/null +++ b/app/services/projects/group_links/destroy_service.rb @@ -0,0 +1,10 @@ +module Projects +  module GroupLinks +    class DestroyService < BaseService +      def execute(group_link) +        return false unless group_link +        group_link.destroy +      end +    end +  end +end diff --git a/app/services/projects/hashed_storage_migration_service.rb b/app/services/projects/hashed_storage_migration_service.rb index 41259de3a16..f5945f3b87f 100644 --- a/app/services/projects/hashed_storage_migration_service.rb +++ b/app/services/projects/hashed_storage_migration_service.rb @@ -10,7 +10,7 @@ module Projects      end      def execute -      return if project.hashed_storage? +      return if project.hashed_storage?(:repository)        @old_disk_path = project.disk_path        has_wiki = project.wiki.repository_exists? diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index c3bf0031409..455b302d819 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -44,7 +44,7 @@ module Projects          else            clone_repository          end -      rescue Gitlab::Shell::Error => e +      rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e          # Expire cache to prevent scenarios such as:          # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true          # 2. Retried import, repo is broken or not imported but +exists?+ still returns true diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index abe414d0c05..c499f384426 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -3,20 +3,26 @@ module Projects      def execute        return unless @project.forked? -      @project.forked_from_project.lfs_objects.find_each do |lfs_object| -        lfs_object.projects << @project +      if fork_source = @project.fork_source +        fork_source.lfs_objects.find_each do |lfs_object| +          lfs_object.projects << @project +        end + +        refresh_forks_count(fork_source)        end -      merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project) +      merge_requests = @project.fork_network +                         .merge_requests +                         .opened +                         .where.not(target_project: @project) +                         .from_project(@project)        merge_requests.each do |mr|          ::MergeRequests::CloseService.new(@project, @current_user).execute(mr)        end -      refresh_forks_count(@project.forked_from_project) - -      @project.forked_project_link.destroy        @project.fork_network_member.destroy +      @project.forked_project_link.destroy      end      def refresh_forks_count(project) diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index a077b3584b0..06ac86cd5a9 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -381,7 +381,7 @@ module QuickActions      end      desc 'Add or substract spent time' -    explanation do |time_spent| +    explanation do |time_spent, time_spent_date|        if time_spent          if time_spent > 0            verb = 'Adds' @@ -394,16 +394,20 @@ module QuickActions          "#{verb} #{Gitlab::TimeTrackingFormatter.output(value)} spent time."        end      end -    params '<1h 30m | -1h 30m>' +    params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>'      condition do        current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)      end -    parse_params do |raw_duration| -      Gitlab::TimeTrackingFormatter.parse(raw_duration) +    parse_params do |raw_time_date| +      Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute      end -    command :spend do |time_spent| +    command :spend do |time_spent, time_spent_date|        if time_spent -        @updates[:spend_time] = { duration: time_spent, user: current_user } +        @updates[:spend_time] = { +          duration: time_spent, +          user: current_user, +          spent_at: time_spent_date +        }        end      end @@ -458,7 +462,7 @@ module QuickActions        target_branch_param.strip      end      command :target_branch do |branch_name| -      @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name) +      @updates[:target_branch] = branch_name if project.repository.branch_exists?(branch_name)      end      desc 'Move issue from one column of the board to another' diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index a1c2f8d0180..911cc919bb8 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -35,24 +35,22 @@ class SystemHooksService          data[:old_path_with_namespace] = model.old_path_with_namespace        end      when User -      data.merge!({ -        name: model.name, -        email: model.email, -        user_id: model.id, -        username: model.username -      }) +      data.merge!(user_data(model)) + +      if event == :rename +        data[:old_username] = model.username_was +      end      when ProjectMember        data.merge!(project_member_data(model))      when Group -      owner = model.owner +      data.merge!(group_data(model)) -      data.merge!( -        name: model.name, -        path: model.path, -        group_id: model.id, -        owner_name: owner.respond_to?(:name) ? owner.name : nil, -        owner_email: owner.respond_to?(:email) ? owner.email : nil -      ) +      if event == :rename +        data.merge!( +          old_path: model.path_was, +          old_full_path: model.full_path_was +        ) +      end      when GroupMember        data.merge!(group_member_data(model))      end @@ -83,7 +81,7 @@ class SystemHooksService        project_id: model.id,        owner_name: owner.name,        owner_email: owner.respond_to?(:email) ? owner.email : "", -      project_visibility: Project.visibility_levels.key(model.visibility_level_value).downcase +      project_visibility: model.visibility.downcase      }    end @@ -104,6 +102,19 @@ class SystemHooksService      }    end +  def group_data(model) +    owner = model.owner + +    { +      name: model.name, +      path: model.path, +      full_path: model.full_path, +      group_id: model.id, +      owner_name: owner.try(:name), +      owner_email: owner.try(:email) +    } +  end +    def group_member_data(model)      {        group_name: model.group.name, @@ -116,4 +127,13 @@ class SystemHooksService        group_access: model.human_access      }    end + +  def user_data(model) +    { +      name: model.name, +      email: model.email, +      user_id: model.id, +      username: model.username +    } +  end  end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 7b32e215c7f..69bd19c1977 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -162,7 +162,6 @@ module SystemNoteService    #   "changed time estimate to 3d 5h"    #    # Returns the created Note object -    def change_time_estimate(noteable, project, author)      parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)      body = if noteable.time_estimate == 0 @@ -188,16 +187,17 @@ module SystemNoteService    #   "added 2h 30m of time spent"    #    # Returns the created Note object -    def change_time_spent(noteable, project, author)      time_spent = noteable.time_spent      if time_spent == :reset        body = "removed time spent"      else +      spent_at = noteable.spent_at        parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)        action = time_spent > 0 ? 'added' : 'subtracted'        body = "#{action} #{parsed_time} of time spent" +      body << " at #{spent_at}" if spent_at      end      create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) @@ -451,10 +451,6 @@ module SystemNoteService      end    end -  def cross_reference?(note_text) -    note_text =~ /\A#{cross_reference_note_prefix}/i -  end -    # Check if a cross-reference is disallowed    #    # This method prevents adding a "mentioned in !1" note on every single commit @@ -484,7 +480,6 @@ module SystemNoteService    # mentioner - Mentionable object    #    # Returns Boolean -    def cross_reference_exists?(noteable, mentioner)      # Initial scope should be system notes of this noteable type      notes = Note.system.where(noteable_type: noteable.class) @@ -593,7 +588,7 @@ module SystemNoteService    def discussion_lock(issuable, author)      action = issuable.discussion_locked? ? 'locked' : 'unlocked' -    body = "#{action} this issue" +    body = "#{action} this #{issuable.class.to_s.titleize.downcase}"      create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action))    end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index b6125cafa83..e694c5761da 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -31,12 +31,12 @@ class TodoService      mark_pending_todos_as_done(issue, current_user)    end -  # When we destroy an issue we should: +  # When we destroy an issuable we should:    #    #  * refresh the todos count cache for the current user    # -  def destroy_issue(issue, current_user) -    destroy_issuable(issue, current_user) +  def destroy_issuable(issuable, user) +    user.update_todos_count_cache    end    # When we reassign an issue we should: @@ -72,14 +72,6 @@ class TodoService      mark_pending_todos_as_done(merge_request, current_user)    end -  # When we destroy a merge request we should: -  # -  #  * refresh the todos count cache for the current user -  # -  def destroy_merge_request(merge_request, current_user) -    destroy_issuable(merge_request, current_user) -  end -    # When we reassign a merge request we should:    #    #  * creates a pending todo for new assignee if merge request is assigned @@ -234,10 +226,6 @@ class TodoService      create_mention_todos(issuable.project, issuable, author, nil, skip_users)    end -  def destroy_issuable(issuable, user) -    user.update_todos_count_cache -  end -    def toggling_tasks?(issuable)      issuable.previous_changes.include?('description') &&        issuable.tasks? && issuable.updated_tasks.any? diff --git a/app/services/users/last_push_event_service.rb b/app/services/users/last_push_event_service.rb index f2bfb60604f..57e446d7f30 100644 --- a/app/services/users/last_push_event_service.rb +++ b/app/services/users/last_push_event_service.rb @@ -16,8 +16,8 @@ module Users          user_cache_key        ] -      if event.project.forked? -        keys << project_cache_key(event.project.forked_from_project) +      if forked_from = event.project.forked_from_project +        keys << project_cache_key(forked_from)        end        keys.each { |key| set_key(key, event.id) } diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb index 3a9c151cf9b..976017dfa82 100644 --- a/app/services/users/migrate_to_ghost_user_service.rb +++ b/app/services/users/migrate_to_ghost_user_service.rb @@ -25,7 +25,7 @@ module Users          user.block          # Reverse the user block if record migration fails -        if !migrate_records && transition +        if !migrate_records_in_transaction && transition            transition.rollback            user.save!          end @@ -36,18 +36,22 @@ module Users      private -    def migrate_records +    def migrate_records_in_transaction        user.transaction(requires_new: true) do          @ghost_user = User.ghost -        migrate_issues -        migrate_merge_requests -        migrate_notes -        migrate_abuse_reports -        migrate_award_emojis +        migrate_records        end      end +    def migrate_records +      migrate_issues +      migrate_merge_requests +      migrate_notes +      migrate_abuse_reports +      migrate_award_emojis +    end +      def migrate_issues        user.issues.update_all(author_id: ghost_user.id)        Issue.where(last_edited_by_id: user.id).update_all(last_edited_by_id: ghost_user.id) diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 7027ac4b5db..d4ba3a028be 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -30,7 +30,7 @@ class FileUploader < GitlabUploader    #    # Returns a String without a trailing slash    def self.dynamic_path_segment(model) -    File.join(CarrierWave.root, base_dir, model.full_path) +    File.join(CarrierWave.root, base_dir, model.disk_path)    end    attr_accessor :model diff --git a/app/validators/abstract_path_validator.rb b/app/validators/abstract_path_validator.rb new file mode 100644 index 00000000000..adbccb65a84 --- /dev/null +++ b/app/validators/abstract_path_validator.rb @@ -0,0 +1,38 @@ +class AbstractPathValidator < ActiveModel::EachValidator +  extend Gitlab::EncodingHelper + +  def self.path_regex +    raise NotImplementedError +  end + +  def self.format_regex +    raise NotImplementedError +  end + +  def self.format_error_message +    raise NotImplementedError +  end + +  def self.full_path(record, value) +    value +  end + +  def self.valid_path?(path) +    encode!(path) +    "#{path}/" =~ path_regex +  end + +  def validate_each(record, attribute, value) +    unless value =~ self.class.format_regex +      record.errors.add(attribute, self.class.format_error_message) +      return +    end + +    full_path = self.class.full_path(record, value) +    return unless full_path + +    unless self.class.valid_path?(full_path) +      record.errors.add(attribute, "#{value} is a reserved name") +    end +  end +end diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb new file mode 100644 index 00000000000..13ec342f399 --- /dev/null +++ b/app/validators/cluster_name_validator.rb @@ -0,0 +1,24 @@ +# ClusterNameValidator +# +# Custom validator for ClusterName. +class ClusterNameValidator < ActiveModel::EachValidator +  def validate_each(record, attribute, value) +    if record.user? +      unless value.present? +        record.errors.add(attribute, " has to be present") +      end +    elsif record.gcp? +      if record.persisted? && record.name_changed? +        record.errors.add(attribute, " can not be changed because it's synchronized with provider") +      end + +      unless value.length >= 1 && value.length <= 63 +        record.errors.add(attribute, " is invalid syntax") +      end + +      unless value =~ Gitlab::Regex.kubernetes_namespace_regex +        record.errors.add(attribute, Gitlab::Regex.kubernetes_namespace_regex_message) +      end +    end +  end +end diff --git a/app/validators/dynamic_path_validator.rb b/app/validators/dynamic_path_validator.rb deleted file mode 100644 index 4688aabc2a8..00000000000 --- a/app/validators/dynamic_path_validator.rb +++ /dev/null @@ -1,53 +0,0 @@ -# DynamicPathValidator -# -# Custom validator for GitLab path values. -# These paths are assigned to `Namespace` (& `Group` as a subclass) & `Project` -# -# Values are checked for formatting and exclusion from a list of illegal path -# names. -class DynamicPathValidator < ActiveModel::EachValidator -  extend Gitlab::EncodingHelper - -  class << self -    def valid_user_path?(path) -      encode!(path) -      "#{path}/" =~ Gitlab::PathRegex.root_namespace_path_regex -    end - -    def valid_group_path?(path) -      encode!(path) -      "#{path}/" =~ Gitlab::PathRegex.full_namespace_path_regex -    end - -    def valid_project_path?(path) -      encode!(path) -      "#{path}/" =~ Gitlab::PathRegex.full_project_path_regex -    end -  end - -  def path_valid_for_record?(record, value) -    full_path = record.respond_to?(:build_full_path) ? record.build_full_path : value - -    return true unless full_path - -    case record -    when Project -      self.class.valid_project_path?(full_path) -    when Group -      self.class.valid_group_path?(full_path) -    else # User or non-Group Namespace -      self.class.valid_user_path?(full_path) -    end -  end - -  def validate_each(record, attribute, value) -    unless value =~ Gitlab::PathRegex.namespace_format_regex -      record.errors.add(attribute, Gitlab::PathRegex.namespace_format_message) -      return -    end - -    unless path_valid_for_record?(record, value) -      record.errors.add(attribute, "#{value} is a reserved name") -    end -  end -end diff --git a/app/validators/namespace_path_validator.rb b/app/validators/namespace_path_validator.rb new file mode 100644 index 00000000000..4a0aa64ae0c --- /dev/null +++ b/app/validators/namespace_path_validator.rb @@ -0,0 +1,19 @@ +class NamespacePathValidator < AbstractPathValidator +  extend Gitlab::EncodingHelper + +  def self.path_regex +    Gitlab::PathRegex.full_namespace_path_regex +  end + +  def self.format_regex +    Gitlab::PathRegex.namespace_format_regex +  end + +  def self.format_error_message +    Gitlab::PathRegex.namespace_format_message +  end + +  def self.full_path(record, value) +    record.build_full_path +  end +end diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb new file mode 100644 index 00000000000..829b596ad3c --- /dev/null +++ b/app/validators/project_path_validator.rb @@ -0,0 +1,19 @@ +class ProjectPathValidator < AbstractPathValidator +  extend Gitlab::EncodingHelper + +  def self.path_regex +    Gitlab::PathRegex.full_project_path_regex +  end + +  def self.format_regex +    Gitlab::PathRegex.project_path_format_regex +  end + +  def self.format_error_message +    Gitlab::PathRegex.project_path_format_message +  end + +  def self.full_path(record, value) +    record.build_full_path +  end +end diff --git a/app/validators/user_path_validator.rb b/app/validators/user_path_validator.rb new file mode 100644 index 00000000000..adf02901802 --- /dev/null +++ b/app/validators/user_path_validator.rb @@ -0,0 +1,15 @@ +class UserPathValidator < AbstractPathValidator +  extend Gitlab::EncodingHelper + +  def self.path_regex +    Gitlab::PathRegex.root_namespace_path_regex +  end + +  def self.format_regex +    Gitlab::PathRegex.namespace_format_regex +  end + +  def self.format_error_message +    Gitlab::PathRegex.namespace_format_message +  end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index dbaed1d09fb..3a4d5ce0b5c 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -530,6 +530,44 @@            = succeed "." do              = link_to "repository storages documentation", help_page_path("administration/repository_storages") +  %fieldset +    %legend Git Storage Circuitbreaker settings +    .form-group +      = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2' +      .col-sm-10 +        = f.number_field :circuitbreaker_access_retries, class: 'form-control' +        .help-block +          = circuitbreaker_access_retries_help_text +    .form-group +      = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2' +      .col-sm-10 +        = f.number_field :circuitbreaker_storage_timeout, class: 'form-control' +        .help-block +          = circuitbreaker_storage_timeout_help_text +    .form-group +      = f.label :circuitbreaker_backoff_threshold, _('Number of failures before backing off'), class: 'control-label col-sm-2' +      .col-sm-10 +        = f.number_field :circuitbreaker_backoff_threshold, class: 'form-control' +        .help-block +          = circuitbreaker_backoff_threshold_help_text +    .form-group +      = f.label :circuitbreaker_failure_wait_time, _('Seconds to wait after a storage failure'), class: 'control-label col-sm-2' +      .col-sm-10 +        = f.number_field :circuitbreaker_failure_wait_time, class: 'form-control' +        .help-block +          = circuitbreaker_failure_wait_time_help_text +    .form-group +      = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2' +      .col-sm-10 +        = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' +        .help-block +          = circuitbreaker_failure_count_help_text +    .form-group +      = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2' +      .col-sm-10 +        = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control' +        .help-block +          = circuitbreaker_failure_reset_time_help_text    %fieldset      %legend Repository Checks diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 3ef8f2a3acb..f0cc4d7ee62 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -42,4 +42,4 @@    .panel.panel-default -    %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: none" } +    %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" } diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index e3a77dfdf10..47cc2d4d27e 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -20,7 +20,7 @@        = visibility_level_icon(group.visibility_level, fw: false)    .avatar-container.s40 -    = image_tag group_icon(group), class: "avatar s40 hidden-xs" +    = group_icon(group, class: "avatar s40 hidden-xs")    .title      = link_to [:admin, group], class: 'group-name' do        = group.full_name diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 3e02f7b1e16..2545cecc721 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -16,7 +16,7 @@        %ul.well-list          %li            .avatar-container.s60 -            = image_tag group_icon(@group), class: "avatar s60" +            = group_icon(@group, class: "avatar s60")          %li            %span.light Name:            %strong= @group.name diff --git a/app/views/admin/hook_logs/_index.html.haml b/app/views/admin/hook_logs/_index.html.haml index 7dd9943190f..91a8c0c62fe 100644 --- a/app/views/admin/hook_logs/_index.html.haml +++ b/app/views/admin/hook_logs/_index.html.haml @@ -24,7 +24,7 @@              %td                = truncate(hook_log.url, length: 50)              %td.light -              #{number_with_precision(hook_log.execution_duration, precision: 2)} ms +              #{number_with_precision(hook_log.execution_duration, precision: 2)} sec              %td.light                = time_ago_with_tooltip(hook_log.created_at)              %td diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 4d8754afdd2..c37d8ac45b9 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -14,7 +14,7 @@              = hidden_field_tag :namespace_id, params[:namespace_id]              - namespace = Namespace.find(params[:namespace_id])              - toggle_text = "#{namespace.kind}: #{namespace.full_path}" -          = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) +          = dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' })            .dropdown-menu.dropdown-select.dropdown-menu-align-right              = dropdown_title('Namespaces')              = dropdown_filter("Search for Namespace") diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index ab4165c0bf2..42f92079d85 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -115,7 +115,7 @@              = f.label :new_namespace_id, "Namespace", class: 'control-label'              .col-sm-10                .dropdown -                = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' }) +                = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' })                  .dropdown-menu.dropdown-select                    = dropdown_title('Namespaces')                    = dropdown_filter("Search for Namespace") diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 43cea1358cc..4965dffab9d 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -52,22 +52,23 @@    %br    - if @runners.any? -    .table-holder -      %table.table -        %thead -          %tr -            %th Type -            %th Runner token -            %th Description -            %th Version -            %th Projects -            %th Jobs -            %th Tags -            %th Last contact -            %th +    .runners-content +      .table-holder +        %table.table +          %thead +            %tr +              %th Type +              %th Runner token +              %th Description +              %th Version +              %th Projects +              %th Jobs +              %th Tags +              %th Last contact +              %th -        - @runners.each do |runner| -          = render "admin/runners/runner", runner: runner -    = paginate @runners, theme: "gitlab" +          - @runners.each do |runner| +            = render "admin/runners/runner", runner: runner +      = paginate @runners, theme: "gitlab"    - else      .nothing-here-block No runners found diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml index 39c7fb0eba2..35a3563dff1 100644 --- a/app/views/ci/status/_badge.html.haml +++ b/app/views/ci/status/_badge.html.haml @@ -5,9 +5,9 @@  - if link && status.has_details?    = link_to status.details_path, class: css_classes, title: title do -    = custom_icon(status.icon) +    = sprite_icon(status.icon)      = status.text  - else    %span{ class: css_classes, title: title } -    = custom_icon(status.icon) +    = sprite_icon(status.icon)      = status.text diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml index dcfb7f0c32d..c5b4439e273 100644 --- a/app/views/ci/status/_dropdown_graph_badge.html.haml +++ b/app/views/ci/status/_dropdown_graph_badge.html.haml @@ -7,13 +7,13 @@  - if status.has_details?    = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' }  do -    %span{ class: klass }= custom_icon(status.icon) +    %span{ class: klass }= sprite_icon(status.icon)      %span.ci-build-text= subject.name  - else    .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } } -    %span{ class: klass }= custom_icon(status.icon) +    %span{ class: klass }= sprite_icon(status.icon)      %span.ci-build-text= subject.name  - if status.has_action? -  = link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' }  do -    = custom_icon(status.action_icon) +  = link_to status.action_path, class: "ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' }  do +    = sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}") diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 7981daa0705..cebdbab4e74 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -1,13 +1,13 @@  .top-area    %ul.nav-links      = nav_link(page: dashboard_groups_path) do -      = link_to dashboard_groups_path, title: 'Your groups' do +      = link_to dashboard_groups_path, title: _("Your groups") do          Your groups      = nav_link(page: explore_groups_path) do -      = link_to explore_groups_path, title: 'Explore public groups' do +      = link_to explore_groups_path, title: _("Explore public groups") do          Explore public groups    .nav-controls      = render 'shared/groups/search_form'      = render 'shared/groups/dropdown'      - if current_user.can_create_group? -      = link_to "New group", new_group_path, class: "btn btn-new" +      = link_to _("New group"), new_group_path, class: "btn btn-new" diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index fd2ba9ac1ca..9038c4fbebd 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -6,13 +6,13 @@    .fade-right= icon('angle-right')    %ul.nav-links.scrolling-tabs      = nav_link(page: [dashboard_projects_path, root_path]) do -      = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do +      = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do          Your projects      = nav_link(page: starred_dashboard_projects_path) do -      = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do +      = link_to starred_dashboard_projects_path, data: {placement: 'right'} do          Starred projects      = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do -      = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do +      = link_to explore_root_path, data: {placement: 'right'} do          Explore projects    .nav-controls diff --git a/app/views/dashboard/groups/_empty_state.html.haml b/app/views/dashboard/groups/_empty_state.html.haml deleted file mode 100644 index f5222fe631e..00000000000 --- a/app/views/dashboard/groups/_empty_state.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -.groups-empty-state -  = custom_icon("icon_empty_groups") - -  .text-content -    %h4 A group is a collection of several projects. -    %p If you organize your projects under a group, it works like a folder. -    %p You can manage your group member’s permissions and access to each project in the group. diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml index 168e6272d8e..601b6a8b1a7 100644 --- a/app/views/dashboard/groups/_groups.html.haml +++ b/app/views/dashboard/groups/_groups.html.haml @@ -1,9 +1,2 @@  .js-groups-list-holder -  #dashboard-group-app{ data: { endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path } } -    .groups-list-loading -      = icon('spinner spin', 'v-show' => 'isLoading') -    %template{ 'v-if' => '!isLoading && isEmpty' } -      %div{ 'v-cloak' => true } -        = render 'empty_state' -    %template{ 'v-else-if' => '!isLoading && !isEmpty' } -      %groups-component{ ':groups' => 'state.groups', ':page-info' => 'state.pageInfo' } +  #js-groups-tree{ data: { hide_projects: 'true', endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } } diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 1cea8182733..25bf08c6c12 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -6,7 +6,7 @@  = webpack_bundle_tag 'common_vue'  = webpack_bundle_tag 'groups' -- if @groups.empty? -  = render 'empty_state' +- if params[:filter].blank? && @groups.empty? +  = render 'shared/groups/empty_state'  - else    = render 'groups' diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index 38fd053ae65..efe1fb99efc 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -36,7 +36,7 @@      .todo-body        .todo-note          .md -          = event_note(todo.body, project: todo.project) +          = first_line_in_markdown(todo, :body, 150, project: todo.project)    - if todo.pending?      .todo-actions diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index f62a0cd681e..a5686002328 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -8,7 +8,7 @@        %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>          = link_to todos_filter_path(state: 'pending') do            %span -            To do +            Todos            %span.badge              = number_with_delimiter(todos_pending_count)        %li.todos-done{ class: active_when(params[:state] == 'done') }> diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml index 52279d0a870..4b6c4581eb3 100644 --- a/app/views/discussions/_diff_discussion.html.haml +++ b/app/views/discussions/_diff_discussion.html.haml @@ -7,4 +7,4 @@      %td.notes_line{ colspan: 2 }      %td.notes_content        .content{ class: ('hide' unless expanded) } -        = render partial: "discussions/notes", collection: discussions, as: :discussion +        = render partial: "discussions/notes", collection: discussions, as: :discussion, locals: { disable_collapse_class: true } diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index 636d06cab53..f9bfc01f213 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -24,4 +24,4 @@      = render partial: "projects/diffs/#{partial}", locals: { diff_file: diff_file, position: discussion.position.to_json, click_to_comment: false }      .note-container -      = render partial: "discussions/notes", locals: { discussion: discussion, show_toggle: false, show_image_comment_badge: true, disable_collapse: true } +      = render partial: "discussions/notes", locals: { discussion: discussion, show_toggle: false, show_image_comment_badge: true, disable_collapse_class: true } diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml index 578e751ab47..0f03163a2e8 100644 --- a/app/views/discussions/_discussion.html.haml +++ b/app/views/discussions/_discussion.html.haml @@ -44,4 +44,4 @@              = render "discussions/diff_with_notes", discussion: discussion            - else              .panel.panel-default -              = render "discussions/notes", discussion: discussion +              = render partial: "discussions/notes", locals: { discussion: discussion, disable_collapse_class: true } diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml index 9efcfef690f..1cc227428e9 100644 --- a/app/views/discussions/_notes.html.haml +++ b/app/views/discussions/_notes.html.haml @@ -1,5 +1,5 @@ -- disable_collapse = local_assigns.fetch(:disable_collapse, false) -- collapsed_class = 'collapsed' if discussion.resolved? && !disable_collapse +- disable_collapse_class = local_assigns.fetch(:disable_collapse_class, false) +- collapsed_class = 'collapsed' if discussion.resolved? && !disable_collapse_class  - badge_counter = discussion_counter + 1 if local_assigns[:discussion_counter]  - show_toggle = local_assigns.fetch(:show_toggle, true)  - show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false) diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml index 253cd336882..079d9083dff 100644 --- a/app/views/discussions/_parallel_diff_discussion.html.haml +++ b/app/views/discussions/_parallel_diff_discussion.html.haml @@ -4,7 +4,7 @@      %td.notes_line.old      %td.notes_content.parallel.old        .content{ class: ('hide' unless discussions_left.any?(&:expanded?)) } -        = render partial: "discussions/notes", collection: discussions_left, as: :discussion, line_type: 'old' +        = render partial: "discussions/notes", collection: discussions_left, as: :discussion, line_type: 'old', locals: { disable_collapse_class: true }    - else      %td.notes_line.old= ("")      %td.notes_content.parallel.old @@ -14,7 +14,7 @@      %td.notes_line.new      %td.notes_content.parallel.new        .content{ class: ('hide' unless discussions_right.any?(&:expanded?)) } -        = render partial: "discussions/notes", collection: discussions_right, as: :discussion, line_type: 'new' +        = render partial: "discussions/notes", collection: discussions_right, as: :discussion, line_type: 'new', locals: { disable_collapse_class: true }    - else      %td.notes_line.new= ("")      %td.notes_content.parallel.new diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml index 6fa2f9bd4db..7e264eb5575 100644 --- a/app/views/events/_event_note.atom.haml +++ b/app/views/events/_event_note.atom.haml @@ -1,2 +1,2 @@  %div{ xmlns: "http://www.w3.org/1999/xhtml" } -  = markdown(note.note, pipeline: :atom, project: note.project, author: note.author) +  = markdown_field(note, :note) diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index df4b9562215..de6383e4097 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -10,7 +10,7 @@  .event-body    .event-note      .md -      = event_note(event.target.note, project: event.project) +      = first_line_in_markdown(event.target, :note, 150, project: event.project)      - note = event.target      - if note.attachment.url        - if note.attachment.image? diff --git a/app/views/explore/groups/_groups.html.haml b/app/views/explore/groups/_groups.html.haml index 794c6d1d170..91149498248 100644 --- a/app/views/explore/groups/_groups.html.haml +++ b/app/views/explore/groups/_groups.html.haml @@ -1,6 +1,2 @@  .js-groups-list-holder -  %ul.content-list -    - @groups.each do |group| -      = render 'shared/groups/group', group: group - -  = paginate @groups, theme: 'gitlab' +  #js-groups-tree{ data: { hide_projects: 'true', endpoint: explore_groups_path(format: :json), path: explore_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } } diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 2651ef37e67..86abdf547cc 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -2,6 +2,9 @@  - page_title    "Groups"  - header_title  "Groups", dashboard_groups_path += webpack_bundle_tag 'common_vue' += webpack_bundle_tag 'groups' +  - if current_user    = render 'dashboard/groups_head'  - else @@ -17,7 +20,7 @@        %p Below you will find all the groups that are public.        %p You can easily contribute to them by requesting to join these groups. -- if @groups.present? -  = render 'groups' -- else +- if params[:filter].blank? && @groups.empty?    .nothing-here-block No public groups +- else +  = render 'groups' diff --git a/app/views/groups/_children.html.haml b/app/views/groups/_children.html.haml new file mode 100644 index 00000000000..3afb6b2f849 --- /dev/null +++ b/app/views/groups/_children.html.haml @@ -0,0 +1,5 @@ += webpack_bundle_tag 'common_vue' += webpack_bundle_tag 'groups' + +.js-groups-list-holder +  #js-groups-tree{ data: { hide_projects: 'false', group_id: group.id, endpoint: group_children_path(group, format: :json), path: group_path(group), form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } } diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml index 181c7bee702..a0760c2073b 100644 --- a/app/views/groups/_home_panel.html.haml +++ b/app/views/groups/_home_panel.html.haml @@ -1,7 +1,7 @@  .group-home-panel.text-center    %div{ class: container_class }      .avatar-container.s70.group-avatar -      = image_tag group_icon(@group), class: "avatar s70 avatar-tile" +      = group_icon(@group, class: "avatar s70 avatar-tile")      %h1.group-title        = @group.name        %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } diff --git a/app/views/groups/_show_nav.html.haml b/app/views/groups/_show_nav.html.haml deleted file mode 100644 index 35b75bc0923..00000000000 --- a/app/views/groups/_show_nav.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -%ul.nav-links -  = nav_link(page: group_path(@group)) do -    = link_to group_path(@group) do -      Projects -  - if Group.supports_nested_groups? -    = nav_link(page: subgroups_group_path(@group)) do -      = link_to subgroups_group_path(@group) do -        Subgroups diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 15606dd30fd..16038ef2f79 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -10,7 +10,7 @@        .form-group          .col-sm-offset-2.col-sm-10            .avatar-container.s160 -            = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160' +            = group_icon(@group, alt: '', class: 'avatar group-avatar s160')            %p.light              - if @group.avatar?                You can change your group avatar here diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 07e64d9aeaf..00909982d59 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -19,13 +19,6 @@    = render 'shared/issuable/search_bar', type: :issues -  .row-content-block.second-block -    Only issues from the -    %strong= @group.name -    group are listed here. -    - if current_user -      To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. -    = render 'shared/issues'  - else    = render 'shared/empty_states/issues', project_select_button: true diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index e56dc1fb9c2..694292aa7c1 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -15,11 +15,4 @@    = render 'shared/issuable/search_bar', type: :merge_requests -  .row-content-block.second-block -    Only merge requests from -    %strong= @group.name -    group are listed here. -    - if current_user -      To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. -    = render 'shared/merge_requests' diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml index 7f450cd9a93..a1be0d3220a 100644 --- a/app/views/groups/milestones/_form.html.haml +++ b/app/views/groups/milestones/_form.html.haml @@ -10,8 +10,8 @@        .form-group.milestone-description          = f.label :description, "Description", class: "control-label"          .col-sm-10 -          = render layout: 'projects/md_preview', locals: { url: '' } do -            = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' +          = render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do +            = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...', supports_autocomplete: false              .clearfix              .error-alert diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 3ca63f9c3e0..7f9486d08d9 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,5 +1,6 @@  - @no_container = true  - breadcrumb_title "Details" +- can_create_subgroups = can?(current_user, :create_subgroup, @group)  = content_for :meta_tags do    = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity") @@ -7,13 +8,38 @@  = render 'groups/home_panel'  .groups-header{ class: container_class } -  .top-area -    = render 'groups/show_nav' -    .nav-controls -      = render 'shared/projects/search_form' -      = render 'shared/projects/dropdown' +  .group-nav-container +    .nav-controls.clearfix +      = render "shared/groups/search_form" +      = render "shared/groups/dropdown", show_archive_options: true        - if can? current_user, :create_projects, @group -        = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do -          New Project +        - new_project_label = _("New project") +        - new_subgroup_label = _("New subgroup") +        - if can_create_subgroups +          .btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } } +            %input.btn.btn-success.dropdown-primary.js-new-group-child{ type: "button", value: new_project_label, data: { action: "new-project" } } +            %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown" } } +              = icon("caret-down", class: "dropdown-btn-icon") +            %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } } +              %li.droplab-item-selected{ role: "button", data: { value: "new-project", text: new_project_label } } +                .menu-item +                  .icon-container +                    = icon("check", class: "list-item-checkmark") +                  .description +                    %strong= new_project_label +                    %span= s_("GroupsTree|Create a project in this group.") +              %li.divider.droplap-item-ignore +              %li{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } } +                .menu-item +                  .icon-container +                    = icon("check", class: "list-item-checkmark") +                  .description +                    %strong= new_subgroup_label +                    %span= s_("GroupsTree|Create a subgroup in this group.") +        - else +          = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success" -  = render "projects", projects: @projects +  - if params[:filter].blank? && !@has_children +    = render "shared/groups/empty_state" +  - else +    = render "children", children: @children, group: @group diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml deleted file mode 100644 index 869b3b243c6..00000000000 --- a/app/views/groups/subgroups.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -- breadcrumb_title "Details" -- @no_container = true - -= render 'groups/home_panel' - -.groups-header{ class: container_class } -  .top-area -    = render 'groups/show_nav' -    .nav-controls -      = form_tag request.path, method: :get do |f| -        = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name', class: 'form-control', spellcheck: false -      - if can?(current_user, :create_subgroup, @group) -        = link_to new_group_path(parent_id: @group.id), class: 'btn btn-new pull-right' do -          New Subgroup - -  - if @nested_groups.present? -    %ul.content-list -      = render partial: 'shared/groups/group', collection: @nested_groups, locals: { full_name: false } -  - else -    .nothing-here-block -      There are no subgroups to show. diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index b18b3dd5766..29b23ae2e52 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -17,10 +17,6 @@                    %th Global Shortcuts                  %tr                    %td.shortcut -                    .key n -                  %td Main Navigation -                %tr -                  %td.shortcut                      .key s                    %td Focus Search                  %tr diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index c25eae63eec..d0c2e0b1d69 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -11,6 +11,7 @@        %span= Gitlab::VERSION        %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION)        = version_status_badge +    %p.slead      GitLab is open source software to collaborate on code.      %br @@ -23,6 +24,7 @@      Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.      %br      Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. +    %p= link_to 'Check the current instance configuration ', help_instance_configuration_url    %hr  .row.prepend-top-default diff --git a/app/views/help/instance_configuration.html.haml b/app/views/help/instance_configuration.html.haml new file mode 100644 index 00000000000..f09e3825a4b --- /dev/null +++ b/app/views/help/instance_configuration.html.haml @@ -0,0 +1,17 @@ +- page_title 'Instance Configuration' +.wiki.documentation +  %h1 Instance Configuration + +  %p +    In this page you will find information about the settings that are used in your current instance. + +  = render 'help/instance_configuration/ssh_info' +  = render 'help/instance_configuration/gitlab_pages' +  = render 'help/instance_configuration/gitlab_ci' +  %p +    %strong Table of contents + +    %ul +      = content_for :table_content + +  = content_for :settings_content diff --git a/app/views/help/instance_configuration/_gitlab_ci.html.haml b/app/views/help/instance_configuration/_gitlab_ci.html.haml new file mode 100644 index 00000000000..7fa8bd086d4 --- /dev/null +++ b/app/views/help/instance_configuration/_gitlab_ci.html.haml @@ -0,0 +1,24 @@ +- content_for :table_content do +  %li= link_to 'GitLab CI', '#gitlab-ci' + +- content_for :settings_content do +  %h2#gitlab-ci +    GitLab CI + +  %p +    Below are the current settings regarding +    = succeed('.') { link_to('GitLab CI', 'https://about.gitlab.com/gitlab-ci', target: '_blank') } + +  .table-responsive +    %table +      %thead +        %tr +          %th Setting +          %th= instance_configuration_host(@instance_configuration.settings[:host]) +          %th Default +      %tbody +        %tr +          - artifacts_size = @instance_configuration.settings[:gitlab_ci][:artifacts_max_size] +          %td Artifacts maximum size +          %td= instance_configuration_human_size_cell(artifacts_size[:value]) +          %td= instance_configuration_human_size_cell(artifacts_size[:default]) diff --git a/app/views/help/instance_configuration/_gitlab_pages.html.haml b/app/views/help/instance_configuration/_gitlab_pages.html.haml new file mode 100644 index 00000000000..bdd77730dcc --- /dev/null +++ b/app/views/help/instance_configuration/_gitlab_pages.html.haml @@ -0,0 +1,35 @@ +- gitlab_pages = @instance_configuration.settings[:gitlab_pages] +- content_for :table_content do +  %li= link_to 'GitLab Pages', '#gitlab-pages' + +- content_for :settings_content do +  %h2#gitlab-pages +    GitLab Pages + +  %p +    Below are the settings for +    = succeed('.') { link_to('Gitlab Pages', gitlab_pages[:url], target: '_blank') } +  .table-responsive +    %table +      %thead +        %tr +          %th Setting +          %th= instance_configuration_host(@instance_configuration.settings[:host]) +      %tbody +        %tr +          %td Domain Name +          %td +            %code= instance_configuration_cell_html(gitlab_pages[:host]) +        %tr +          %td IP Address +          %td +            %code= instance_configuration_cell_html(gitlab_pages[:ip_address]) +        %tr +          %td Port +          %td +            %code= instance_configuration_cell_html(gitlab_pages[:port]) +  %br + +  %p +    The maximum size of your Pages site is regulated by the artifacts maximum +    size which is part of #{succeed('.') { link_to('GitLab CI', '#gitlab-ci') }} diff --git a/app/views/help/instance_configuration/_ssh_info.html.haml b/app/views/help/instance_configuration/_ssh_info.html.haml new file mode 100644 index 00000000000..987cc61b3f6 --- /dev/null +++ b/app/views/help/instance_configuration/_ssh_info.html.haml @@ -0,0 +1,27 @@ +- ssh_info = @instance_configuration.settings[:ssh_algorithms_hashes] +- if ssh_info.any? +  - content_for :table_content do +    %li= link_to 'SSH host keys fingerprints', '#ssh-host-keys-fingerprints' + +  - content_for :settings_content do +    %h2#ssh-host-keys-fingerprints +      SSH host keys fingerprints + +    %p +      Below are the fingerprints for the current instance SSH host keys. + +    .table-responsive +      %table +        %thead +          %tr +            %th Algorithm +            %th MD5 +            %th SHA256 +        %tbody +          - ssh_info.each do |algorithm| +            %tr +              %td= algorithm[:name] +              %td +                %code= instance_configuration_cell_html(algorithm[:md5]) +              %td +                %code= instance_configuration_cell_html(algorithm[:sha256]) diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index f1b32274664..1597621fa78 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -37,7 +37,7 @@    - if content_for?(:library_javascripts)      = yield :library_javascripts -  = javascript_include_tag asset_path("locale/#{I18n.locale.to_s || I18n.default_locale.to_s}/app.js") unless I18n.locale == :en +  = javascript_include_tag locale_path unless I18n.locale == :en    = webpack_bundle_tag "webpack_runtime"    = webpack_bundle_tag "common"    = webpack_bundle_tag "main" diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 29387d6627e..4c5cc249159 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -5,7 +5,7 @@  - if @group && @group.persisted? && @group.path    - group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }  - if @project && @project.persisted? -  - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project) } +  - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project), issues_disabled: !@project.issues_enabled? }  .search.search-form{ class: "#{'has-location-badge' if label.present?}" }    = form_tag search_path, method: :get, class: 'navbar-form' do |f|      .search-input-container diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 8cba495f7e4..0bf318b0b66 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -6,7 +6,7 @@      .context-header        = link_to group_path(@group), title: @group.name do          .avatar-container.s40.group-avatar -          = image_tag group_icon(@group), class: "avatar s40 avatar-tile" +          = group_icon(@group, class: "avatar s40 avatar-tile")          .sidebar-context-title            = @group.name      %ul.sidebar-top-level-items diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 759d6ff68ea..66146e61263 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -272,6 +272,11 @@                = sprite_icon('users')              %span.nav-item-name                Members +          %ul.sidebar-sub-level-items.is-fly-out-only +            = nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do +              = link_to project_project_members_path(@project) do +                %strong.fly-out-top-item-name +                  #{ _('Members') }        = render 'shared/sidebar_toggle_button' diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index b7a60938132..8eb3f2d5192 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -31,7 +31,7 @@                %tbody                  %tr                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } -                    %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ +                    %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "" }/                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }                      %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }                        = @pipeline.ref @@ -42,7 +42,7 @@                %tbody                  %tr                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } -                    %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ +                    %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "" }/                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }                      %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }                        = @pipeline.short_sha @@ -60,7 +60,7 @@                %tbody                  %tr                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } -                    %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ +                    %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }                      - if commit.author                        %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } @@ -76,7 +76,7 @@                  %tbody                    %tr                      %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } -                      %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ +                      %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/                      %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }                        - if commit.committer                          %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } @@ -100,7 +100,7 @@              triggered by            - if @pipeline.user              %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } -              %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ +              %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/              %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }                %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }                  = @pipeline.user.name diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 3f16885b8e3..574a8f2fa50 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -31,7 +31,7 @@                %tbody                  %tr                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } -                    %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ +                    %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "" }/                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }                      %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }                        = @pipeline.ref @@ -42,7 +42,7 @@                %tbody                  %tr                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } -                    %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ +                    %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "" }/                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }                      %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }                        = @pipeline.short_sha @@ -60,7 +60,7 @@                %tbody                  %tr                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } -                    %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ +                    %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }                      - if commit.author                        %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } @@ -76,7 +76,7 @@                  %tbody                    %tr                      %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } -                      %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ +                      %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/                      %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }                        - if commit.committer                          %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } @@ -100,7 +100,7 @@              triggered by            - if @pipeline.user              %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } -              %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ +              %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/              %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }                %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }                  = @pipeline.user.name diff --git a/app/views/peek/views/_gitaly.html.haml b/app/views/peek/views/_gitaly.html.haml new file mode 100644 index 00000000000..a7d040d6821 --- /dev/null +++ b/app/views/peek/views/_gitaly.html.haml @@ -0,0 +1,7 @@ +- local_assigns.fetch(:view) + +%strong +  %span{ data: { defer_to: "#{view.defer_key}-duration" } } ... +  \/ +  %span{ data: { defer_to: "#{view.defer_key}-calls" } } ... +  Gitaly diff --git a/app/views/profiles/accounts/_reset_token.html.haml b/app/views/profiles/accounts/_reset_token.html.haml deleted file mode 100644 index c31a4a8ecd4..00000000000 --- a/app/views/profiles/accounts/_reset_token.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- name = label.parameterize -- attribute = name.underscore - -.reset-action -  %p.cgray -    = label_tag name, label, class: "label-light" -    = text_field_tag name, current_user.send(attribute), class: 'form-control', readonly: true, onclick: 'this.select()' -    %p.help-block -      = help_text -  .prepend-top-default -    = link_to button_label, [:reset, attribute, :profile], method: :put, data: { confirm: 'Are you sure?' }, class: 'btn btn-default private-token' diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 7f79168dfb3..ced58dffcdc 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -9,22 +9,6 @@  .row.prepend-top-default    .col-lg-4.profile-settings-sidebar      %h4.prepend-top-0 -      Private Tokens -    %p -      Keep these tokens secret, anyone with access to them can interact with -      GitLab as if they were you. -  .col-lg-8.private-tokens-reset -    = render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' } - -    = render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' } - -    - if incoming_email_token_enabled? -      = render partial: 'reset_token', locals: { label: 'Incoming email token', button_label: 'Reset incoming email token', help_text: 'Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.' } - -%hr -.row.prepend-top-default -  .col-lg-4.profile-settings-sidebar -    %h4.prepend-top-0        Two-Factor Authentication      %p        Increase your account's security by enabling Two-Factor Authentication (2FA). diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 06bb72b9f0d..26c2e4c5936 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -30,3 +30,40 @@      = render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes      = render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens + +%hr +.row.prepend-top-default +  .col-lg-4.profile-settings-sidebar +    %h4.prepend-top-0 +      RSS token +    %p +      Your RSS token is used to authenticate you when your RSS reader loads a personalized RSS feed, and is included in your personal RSS feed URLs. +    %p +      It cannot be used to access any other data. +  .col-lg-8.rss-token-reset +    = label_tag :rss_token, 'RSS token', class: "label-light" +    = text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()' +    %p.help-block +      Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you. +      You should +      = link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' } +      if that ever happens. + +- if incoming_email_token_enabled? +  %hr +  .row.prepend-top-default +    .col-lg-4.profile-settings-sidebar +      %h4.prepend-top-0 +        Incoming email token +      %p +        Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses. +      %p +        It cannot be used to access any other data. +    .col-lg-8.incoming-email-token-reset +      = label_tag :incoming_email_token, 'Incoming email token', class: "label-light" +      = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()' +      %p.help-block +        Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. +        You should +        = link_to 'reset it', [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: 'Are you sure? Any issue email addresses currently in use will stop working.' } +        if that ever happens. diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml index 623d3bc91c6..c5b1897c492 100644 --- a/app/views/projects/_export.html.haml +++ b/app/views/projects/_export.html.haml @@ -3,7 +3,7 @@  - project = local_assigns.fetch(:project)  - expanded = Rails.env.test? -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) }    .settings-header      %h4        Export project @@ -11,7 +11,7 @@        = expanded ? 'Collapse' : 'Expand'      %p        Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. -  .settings-content.no-animate{ class: ('expanded' if expanded) } +  .settings-content      .bs-callout.bs-callout-info        %p.append-bottom-0          %p diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 619b632918e..1d644dda177 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,6 +1,5 @@  - empty_repo = @project.empty_repo?  - fork_network = @project.fork_network -- forked_from_project = @project.forked_from_project || fork_network&.root_project  .project-home-panel.text-center{ class: ("empty-project" if empty_repo) }    .limit-container-width{ class: container_class }      .avatar-container.s70.project-avatar @@ -16,13 +15,13 @@        - if @project.forked?          %p -          - if forked_from_project +          - if @project.fork_source              #{ s_('ForkedFromProjectPath|Forked from') } -            = link_to project_path(forked_from_project) do -              = forked_from_project.full_name +            = link_to project_path(@project.fork_source) do +              = fork_source_name(@project)            - else              - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') -            = deleted_message % { project_name: fork_network.deleted_root_project_name } +            = deleted_message % { project_name: fork_source_name(@project) }      .project-repo-buttons        .count-buttons diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 770608eddff..f8a2ea18989 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -19,16 +19,16 @@        %li.pull-right          .toolbar-group -          = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) -          = markdown_toolbar_button({ icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" }) -          = markdown_toolbar_button({ icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) -          = markdown_toolbar_button({ icon: "code fw", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) -          = markdown_toolbar_button({ icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) -          = markdown_toolbar_button({ icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) -          = markdown_toolbar_button({ icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) +          = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" }) +          = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" }) +          = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) +          = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) +          = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) +          = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) +          = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })          .toolbar-group            %button.toolbar-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } } -            = icon("arrows-alt fw") +            = sprite_icon("screen-full")    .md-write-holder      = yield diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml new file mode 100644 index 00000000000..a78a8e5d628 --- /dev/null +++ b/app/views/projects/_new_project_fields.html.haml @@ -0,0 +1,41 @@ +- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility + +.row{ id: project_name_id } +  .form-group.project-path.col-sm-6 +    = f.label :namespace_id, class: 'label-light' do +      %span +        Project path +    .input-group +      - if current_user.can_select_namespace? +        .input-group-addon +          = root_url +        = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1} + +      - else +        .input-group-addon.static-namespace +          #{user_url(current_user.username)}/ +        = f.hidden_field :namespace_id, value: current_user.namespace_id +  .form-group.project-path.col-sm-6 +    = f.label :path, class: 'label-light' do +      %span +        Project name +    = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true +- if current_user.can_create_group? +  .help-block +    Want to house several dependent projects under the same namespace? +    = link_to "Create a group", new_group_path + +.form-group +  = f.label :description, class: 'label-light' do +    Project description +    %span (optional) +  = f.text_area :description, placeholder: 'Description format',  class: "form-control", rows: 3, maxlength: 250 + +.form-group.visibility-level-setting +  = f.label :visibility_level, class: 'label-light' do +    Visibility Level +    = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' } +  = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false + += f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 += link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml index 5638b7da1b0..d50175727be 100644 --- a/app/views/projects/_project_templates.html.haml +++ b/app/views/projects/_project_templates.html.haml @@ -1,10 +1,24 @@ -.project-templates-buttons.import-buttons{ data: { toggle: "buttons" } } -  .btn.blank-option.active -    %input{ type: "radio",  autocomplete: "off", name: "project[template_name]", id: "blank", checked: "true", value: "" } -    = icon('file-o', class: 'btn-template-icon') -    Blank +.project-templates-buttons.import-buttons    - Gitlab::ProjectTemplate.all.each do |template| -    .btn -      %input{ type: "radio",  autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name } +    .template-option        = custom_icon(template.logo) -      = template.title +      .template-title= template.title +      .template-description= template.description +      %label.btn.btn-success.template-button.choose-template.append-right-10{ for: template.name } +        %input{ type: "radio",  autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name } +        %span Use template +      %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' } Preview + +  .project-fields-form +    .form-group +      %label.label-light +        Template +      .input-group.template-input-group +        .input-group-addon +          .selected-icon +            - Gitlab::ProjectTemplate.all.each do |template| +              = custom_icon(template.logo) +          .selected-template +        %button.btn.btn-default.change-template{ type: "button" } Change template + +    = render 'new_project_fields', f: f, project_name_id: "template-project-name" diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml new file mode 100644 index 00000000000..44aa9eb3826 --- /dev/null +++ b/app/views/projects/_readme.html.haml @@ -0,0 +1,23 @@ +- if (readme = @repository.readme) && readme.rich_viewer +  %article.file-holder.readme-holder{ id: 'readme', class: ("limited-width-container" unless fluid_layout) } +    .js-file-title.file-title +      = blob_icon readme.mode, readme.name +      = link_to project_blob_path(@project, tree_join(@ref, readme.path)) do +        %strong +          = readme.name +    = render 'projects/blob/viewer', viewer: readme.rich_viewer, viewer_url: namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, readme.path), viewer: :rich, format: :json) + +- else +  .row-content-block.second-block.center +    %h3.page-title +      This project does not have a README yet +    - if can?(current_user, :push_code, @project) +      %p +        A +        %code README +        file contains information about other files in a repository and is commonly +        distributed with computer software, forming part of its documentation. +      %p +        We recommend you to +        = link_to "add a README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link' +        file to the repository and GitLab will render it here instead of this message. diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 4b344b2edb9..7777f55ddd7 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -1,6 +1,6 @@  - action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create' -.file-holder.file.append-bottom-default +.file-holder-bottom-radius.file-holder.file.append-bottom-default    .js-file-title.file-title.clearfix{ data: { current_action: action } }      .editor-ref        = icon('code-fork') diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 49101d1efa4..6e02ae6c9cc 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,3 +1,4 @@ +- merged = local_assigns.fetch(:merged, false)  - commit = @repository.commit(branch.dereferenced_target)  - bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0  - diverging_commit_counts = @repository.diverging_commit_counts(branch) @@ -12,7 +13,7 @@             - if branch.name == @repository.root_ref        %span.label.label-primary default -    - elsif @repository.merged_to_root_ref? branch.name +    - elsif merged        %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }          = s_('Branches|merged') @@ -47,7 +48,7 @@                  target: "#modal-delete-branch",                  delete_path: project_branch_path(@project, branch.name),                  branch_name: branch.name, -                is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } } +                is_merged: ("true" if merged) } }                = icon("trash-o")            - else              %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 7d9645d79e6..aade310236e 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -38,7 +38,7 @@    - if @branches.any?      %ul.content-list.all-branches        - @branches.each do |branch| -        = render "projects/branches/branch", branch: branch +        = render "projects/branches/branch", branch: branch, merged: @repository.merged_to_root_ref?(branch, @merged_branch_names)      = paginate @branches, theme: 'gitlab'    - else      .nothing-here-block diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml new file mode 100644 index 00000000000..97532f1e2bd --- /dev/null +++ b/app/views/projects/clusters/_advanced_settings.html.haml @@ -0,0 +1,14 @@ +- if can?(current_user, :admin_cluster, @cluster) +  .append-bottom-20 +    %label.append-bottom-10 +      = s_('ClusterIntegration|Google Container Engine') +    %p +      - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') +      = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke } + +  .well.form-group +    %label.text-danger +      = s_('ClusterIntegration|Remove cluster integration') +    %p +      = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine.') +    = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"}) diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml index 371cdb1e403..1f8ae463d0f 100644 --- a/app/views/projects/clusters/_form.html.haml +++ b/app/views/projects/clusters/_form.html.haml @@ -4,34 +4,32 @@        - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')        = s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page} -      = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field| +      = form_for @cluster, url: namespace_project_clusters_path(@project.namespace, @project, @cluster), as: :cluster do |field| +        = field.hidden_field :provider_type, value: :gcp          = form_errors(@cluster)          .form-group -          = field.label :gcp_cluster_name, s_('ClusterIntegration|Cluster name') -          = field.text_field :gcp_cluster_name, class: 'form-control' +          = field.label :name, s_('ClusterIntegration|Cluster name') +          = field.text_field :name, class: 'form-control' -        .form-group -          = field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') -          = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer') -          = field.text_field :gcp_project_id, class: 'form-control' - -        .form-group -          = field.label :gcp_cluster_zone, s_('ClusterIntegration|Zone') -          = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer') -          = field.text_field :gcp_cluster_zone, class: 'form-control', placeholder: 'us-central1-a' +        = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field| +          .form-group +            = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') +            = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer') +            = provider_gcp_field.text_field :gcp_project_id, class: 'form-control' -        .form-group -          = field.label :gcp_cluster_size, s_('ClusterIntegration|Number of nodes') -          = field.text_field :gcp_cluster_size, class: 'form-control', placeholder: '3' +          .form-group +            = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone') +            = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer') +            = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a' -        .form-group -          = field.label :gcp_machine_type, s_('ClusterIntegration|Machine type') -          = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer') -          = field.text_field :gcp_machine_type, class: 'form-control', placeholder: 'n1-standard-4' +          .form-group +            = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes') +            = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'            .form-group -          = field.label :project_namespace, s_('ClusterIntegration|Project namespace (optional, unique)') -          = field.text_field :project_namespace, class: 'form-control', placeholder: @cluster.project_namespace_placeholder +            = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type') +            = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer') +            = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-2'          .form-group            = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save' diff --git a/app/views/projects/clusters/_header.html.haml b/app/views/projects/clusters/_header.html.haml index 0134d46491c..beb798e7154 100644 --- a/app/views/projects/clusters/_header.html.haml +++ b/app/views/projects/clusters/_header.html.haml @@ -11,4 +11,4 @@        = s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }      %li        - link_to_container_project = link_to(s_('ClusterIntegration|Google Container Engine project'), target: '_blank', rel: 'noopener noreferrer') -      = s_('ClusterIntegration|A %{link_to_container_project} must have been created under this account').html_safe % { link_to_container_project: link_to_container_project } +      = s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project } diff --git a/app/views/projects/clusters/login.html.haml b/app/views/projects/clusters/login.html.haml index ae132672b7e..fde030b500b 100644 --- a/app/views/projects/clusters/login.html.haml +++ b/app/views/projects/clusters/login.html.haml @@ -10,7 +10,7 @@    .col-sm-8.col-sm-offset-4.signin-with-google      - if @authorize_url        = link_to @authorize_url do -        = image_tag('auth_buttons/signin_with_google.png') +        = image_tag('auth_buttons/signin_with_google.png', width: '191px')      - else        - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer')        = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link } diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index c538d41ffad..6b321f60212 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -1,9 +1,20 @@  - breadcrumb_title "Cluster" -- page_title _("New Cluster") +- page_title _("Cluster")  .row.prepend-top-default    .col-sm-4      = render 'sidebar'    .col-sm-8 -    = render 'header' -= render 'form' +    - if @project.kubernetes_service&.active? +      %h4.prepend-top-0= s_('ClusterIntegration|Cluster management') + +      %p= s_('ClusterIntegration|A cluster has been set up on this project through the Kubernetes integration page') +      = link_to s_('ClusterIntegration|Manage Kubernetes integration'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20' + +    - else +      %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration') + +      %p= s_('ClusterIntegration|Create a new cluster on Google Container Engine right from GitLab') +      = link_to s_('ClusterIntegration|Create on GKE'), providers_gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20' +      %p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster') +      = link_to s_('ClusterIntegration|Add an existing cluster'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20' diff --git a/app/views/projects/clusters/new_gcp.html.haml b/app/views/projects/clusters/new_gcp.html.haml new file mode 100644 index 00000000000..48e6b6ae8e8 --- /dev/null +++ b/app/views/projects/clusters/new_gcp.html.haml @@ -0,0 +1,10 @@ +- breadcrumb_title "Cluster" +- page_title _("New Cluster") + +.row.prepend-top-default +  .col-sm-4 +    = render 'sidebar' +  .col-sm-8 +    = render 'header' + += render 'form' diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index aee6f904a62..b7671f5e3c4 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -1,26 +1,46 @@ +- @content_class = "limit-container-width" unless fluid_layout  - breadcrumb_title "Cluster"  - page_title _("Cluster") -- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) && @cluster.on_creation? -.row.prepend-top-default.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path, +- expanded = Rails.env.test? + +- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) +.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path, +  install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm), +  install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress),    toggle_status: @cluster.enabled? ? 'true': 'false',    cluster_status: @cluster.status_name, -  cluster_status_reason: @cluster.status_reason } } -  .col-sm-4 -    = render 'sidebar' -  .col-sm-8 -    %label.append-bottom-10{ for: 'enable-cluster-integration' } -      = s_('ClusterIntegration|Enable cluster integration') -    %p -      - if @cluster.enabled? -        - if can?(current_user, :update_cluster, @cluster) -          = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.') +  cluster_status_reason: @cluster.status_reason, +  help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications') } } + + +  .js-cluster-application-notice +    .flash-container + +  %section.settings.no-animate.expanded +    %h4= s_('ClusterIntegration|Enable cluster integration') +    .settings-content + +      .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } +        = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine') +        %p.js-error-reason + +      .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' } +        = s_('ClusterIntegration|Cluster is being created on Google Container Engine...') + +      .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' } +        = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine') + +      %p +        - if @cluster.enabled? +          - if can?(current_user, :update_cluster, @cluster) +            = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.') +          - else +            = s_('ClusterIntegration|Cluster integration is enabled for this project.')          - else -          = s_('ClusterIntegration|Cluster integration is enabled for this project.') -      - else -        = s_('ClusterIntegration|Cluster integration is disabled for this project.') +          = s_('ClusterIntegration|Cluster integration is disabled for this project.') -    = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field| +    = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|        = form_errors(@cluster)        .form-group.append-bottom-20          %label.append-bottom-10 @@ -34,37 +54,32 @@          - if can?(current_user, :update_cluster, @cluster)            .form-group -            = field.submit s_('ClusterIntegration|Save'), class: 'btn btn-success' +            = field.submit _('Save'), class: 'btn btn-success' -    - if can?(current_user, :admin_cluster, @cluster) -      %label.append-bottom-10{ for: 'google-container-engine' } -        = s_('ClusterIntegration|Google Container Engine') -      %p -        - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') -        = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke } +  .cluster-applications-table#js-cluster-applications -      .hidden.js-cluster-error.alert.alert-danger.alert-block{ role: 'alert' } -        = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine') -        %p.js-error-reason +  %section.settings#js-cluster-details +    .settings-header +      %h4= s_('ClusterIntegration|Cluster details') +      %button.btn.js-settings-toggle +        = expanded ? 'Collapse' : 'Expand' +      %p= s_('ClusterIntegration|See and edit the details for your cluster') -      .hidden.js-cluster-creating.alert.alert-info.alert-block{ role: 'alert' } -        = s_('ClusterIntegration|Cluster is being created on Google Container Engine...') +    .settings-content -      .hidden.js-cluster-success.alert.alert-success.alert-block{ role: 'alert' } -        = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine') +      .form_group.append-bottom-20 +        %label.append-bottom-10{ for: 'cluster-name' } +          = s_('ClusterIntegration|Cluster name') +        .input-group +          %input.form-control.cluster-name{ value: @cluster.name, disabled: true } +          %span.input-group-addon.clipboard-addon +            = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name')) -    .form_group.append-bottom-20 -      %label.append-bottom-10{ for: 'cluter-name' } -        = s_('ClusterIntegration|Cluster name') -      .input-group -        %input.form-control.cluster-name{ value: @cluster.gcp_cluster_name, disabled: true } -        %span.input-group-addon.clipboard-addon -          = clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name')) - -    - if can?(current_user, :admin_cluster, @cluster) -      .well.form_group -        %label.text-danger -          = s_('ClusterIntegration|Remove cluster integration') -        %p -          = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project.') -        = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"}) +  %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) } +    .settings-header +      %h4= _('Advanced settings') +      %button.btn.js-settings-toggle +        = expanded ? 'Collapse' : 'Expand' +      %p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project') +    .settings-content +      = render 'advanced_settings' diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml index 83821326aec..1d6a0fa38ca 100644 --- a/app/views/projects/commit/_ajax_signature.html.haml +++ b/app/views/projects/commit/_ajax_signature.html.haml @@ -1,2 +1,2 @@  - if commit.has_signature? -  %button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } +  %a{ href: '#', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 09bcd187e59..8b9c1bbb602 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -61,13 +61,13 @@      %span.cgray= n_('parent', 'parents', @commit.parents.count)      - @commit.parents.each do |parent|        = link_to parent.short_id, project_commit_path(@project, parent), class: "commit-sha" -    %span.commit-info.branches +    .commit-info.branches        %i.fa.fa-spinner.fa-spin    - if @commit.last_pipeline      - last_pipeline = @commit.last_pipeline      .well-segment.pipeline-info -      .status-icon-container{ class: "ci-status-icon-#{@commit.status}" } +      .status-icon-container{ class: "ci-status-icon-#{last_pipeline.status}" }          = link_to project_pipeline_path(@project, last_pipeline.id) do            = ci_icon_for_status(last_pipeline.status)        #{ _('Pipeline') } @@ -77,5 +77,6 @@          #{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) }          .mr-widget-pipeline-graph            = render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph' -      in -      = time_interval_in_words last_pipeline.duration +      - if last_pipeline.duration +        in +        = time_interval_in_words last_pipeline.duration diff --git a/app/views/projects/commit/_limit_exceeded_message.html.haml b/app/views/projects/commit/_limit_exceeded_message.html.haml new file mode 100644 index 00000000000..84a52d49487 --- /dev/null +++ b/app/views/projects/commit/_limit_exceeded_message.html.haml @@ -0,0 +1,8 @@ +.has-tooltip{ class: "limit-box limit-box-#{objects} prepend-left-5", data: { title: "Project has too many #{label_for_message} to search"} } +  .limit-icon +    - if objects == :branch +      = icon('code-fork') +    - else +      = icon('tag') +  .limit-message +    %span #{label_for_message.capitalize} unavailable diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index edff018ba6d..b6b7aae6f9a 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -24,5 +24,5 @@    = link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') -%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } } +%a{ href: '#', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }    = label diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml index 911c9ddce06..8611129b356 100644 --- a/app/views/projects/commit/branches.html.haml +++ b/app/views/projects/commit/branches.html.haml @@ -1,15 +1,15 @@ -- if @branches.any? || @tags.any? +- if @branches_limit_exceeded +  = render 'limit_exceeded_message', objects: :branch, label_for_message: "branches" +- elsif @branches.any?    - branch = commit_default_branch(@project, @branches) -  = link_to(project_ref_path(@project, branch), class: "label label-gray ref-name") do -    = icon('code-fork') -    = branch +  = commit_branch_link(project_ref_path(@project, branch), branch) -  -# `commit_default_branch` deletes the default branch from `@branches`, -  -# so only render this if we have more branches left -  - if @branches.any? || @tags.any? -    %span -      = link_to "…", "#", class: "js-details-expand label label-gray" - -      %span.js-details-content.hide -        = commit_branches_links(@project, @branches) if @branches.any? -        = commit_tags_links(@project, @tags) if @tags.any? +- if @branches.any? || @tags.any? || @tags_limit_exceeded +  %span +    = link_to "…", "#", class: "js-details-expand label label-gray" +    %span.js-details-content.hide +      = commit_branches_links(@project, @branches) +      - if @tags_limit_exceeded +        = render 'limit_exceeded_message', objects: :tag, label_for_message: "tags" +      - else +        = commit_tags_links(@project, @tags) diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 45985a5ecef..e75ae87e771 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -1,5 +1,5 @@  - expanded = Rails.env.test? -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) }    .settings-header      %h4        Deploy Keys @@ -7,7 +7,7 @@        = expanded ? 'Collapse' : 'Expand'      %p        Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. -  .settings-content.no-animate{ class: ('expanded' if expanded) } +  .settings-content      %h5.prepend-top-0        Create a new deploy key for this project      = render @deploy_keys.form_partial_path diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 8ae4fd94146..5ebeae5c35f 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -4,7 +4,7 @@  - expanded = Rails.env.test?  .project-edit-container -  %section.settings.general-settings +  %section.settings.general-settings.no-animate{ class: ('expanded' if expanded) }      .settings-header        %h4          General project settings @@ -12,7 +12,7 @@          = expanded ? 'Collapse' : 'Expand'        %p          Update your project name, description, avatar, and other general settings. -    .settings-content.no-animate{ class: ('expanded' if expanded) } +    .settings-content        .project-edit-errors        = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|          %fieldset @@ -61,7 +61,7 @@                = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"          = f.submit 'Save changes', class: "btn btn-save" -  %section.settings.sharing-permissions +  %section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }      .settings-header        %h4          Permissions @@ -69,13 +69,13 @@          = expanded ? 'Collapse' : 'Expand'        %p          Enable or disable certain project features and choose access levels. -    .settings-content.no-animate{ class: ('expanded' if expanded) } +    .settings-content        = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|          %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)          .js-project-permissions-form          = f.submit 'Save changes', class: "btn btn-save" -  %section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) } +  %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }      .settings-header        %h4          Merge request settings @@ -83,22 +83,22 @@          = expanded ? 'Collapse' : 'Expand'        %p          Customize your merge request restrictions. -    .settings-content.no-animate{ class: ('expanded' if expanded) } +    .settings-content        = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f|          = render 'merge_request_settings', form: f          = f.submit 'Save changes', class: "btn btn-save"    = render 'export', project: @project -  %section.settings.advanced-settings +  %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) }      .settings-header        %h4          Advanced settings        %button.btn.js-settings-toggle          = expanded ? 'Collapse' : 'Expand'        %p -        Perform advanced options such as housekeeping, exporting, archiving, renaming, transferring, or removing your project. -    .settings-content.no-animate{ class: ('expanded' if expanded) } +        Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project. +    .settings-content        .sub-section          %h4 Housekeeping          %p @@ -173,7 +173,10 @@            %p              This will remove the fork relationship to source project              = succeed "." do -              = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) +              - if @project.fork_source +                = link_to(fork_source_name(@project), project_path(@project.fork_source)) +              - else +                = fork_source_name(@project)            = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f|              %p                %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 3f3ce10419f..af564b93dc3 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -24,10 +24,15 @@      %p        You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected. +    - if show_auto_devops_callout?(@project) +      %p +        - link = link_to(s_('AutoDevOps|Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings')) +        = s_('AutoDevOps|You can activate %{link_to_settings} for this project.').html_safe % { link_to_settings: link } +      %p +        = s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') +  - if can?(current_user, :push_code, @project)    %div{ class: container_class } -    - if show_auto_devops_callout?(@project) -      = render 'shared/auto_devops_callout'      .prepend-top-20      .empty_wrapper        %h3.page-title-empty @@ -67,6 +72,7 @@            %pre.light-well              :preserve                cd existing_repo +              git remote rename origin old-origin                git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}                git push -u origin --all                git push -u origin --tags diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 70156c03e3c..cce16bc58b3 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,5 +1,5 @@  - @no_container = true -- page_title "Contributors" +- page_title _('Contributors')  - content_for :page_specific_javascripts do    = webpack_bundle_tag('common_d3')    = webpack_bundle_tag('graphs') @@ -7,23 +7,23 @@  .js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) }    .sub-header-block -    .tree-ref-holder +    .tree-ref-holder.inline.vertical-align-middle        = render 'shared/ref_switcher', destination: 'graphs' -    %ul.breadcrumb.repo-breadcrumb -      = commits_breadcrumbs +    = link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn'    .loading-graph      .center        %h3.page-title          %i.fa.fa-spinner.fa-spin -        Building repository graph. -      %p.slead Please wait a moment, this page will automatically refresh when ready. +        = s_('ContributorsPage|Building repository graph.') +      %p.slead +        = s_('ContributorsPage|Please wait a moment, this page will automatically refresh when ready.')    .stat-graph.hide      .header.clearfix        %h3#date_header.page-title        %p.light -        Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. +        = s_('ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits.') % { branch_name: @ref }        %input#brush_change{ :type => "hidden" }      .graphs.row        #contributors-master diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml index 05b06cfc8b2..8096d9530c3 100644 --- a/app/views/projects/hook_logs/_index.html.haml +++ b/app/views/projects/hook_logs/_index.html.haml @@ -24,7 +24,7 @@              %td                = truncate(hook_log.url, length: 50)              %td.light -              #{number_with_precision(hook_log.execution_duration, precision: 2)} ms +              #{number_with_precision(hook_log.execution_duration, precision: 2)} sec              %td.light                = time_ago_with_tooltip(hook_log.created_at)              %td diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index 13809da6523..0d39edb7bfd 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -3,8 +3,8 @@  - if @can_bulk_update    = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"  = link_to "New issue", new_project_issue_path(@project, -                                              issue: { assignee_id: issues_finder.assignee.try(:id), -                                                       milestone_id: issues_finder.milestones.first.try(:id) }), +                                              issue: { assignee_id: finder.assignee.try(:id), +                                                       milestone_id: finder.milestones.first.try(:id) }),                                                class: "btn btn-new",                                                title: "New issue",                                                id: "new_issue_link" diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index e1b4a49850a..4f78102be0c 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -1,3 +1,7 @@ +- can_create_merge_request = can?(current_user, :create_merge_request, @project) +- data_action = can_create_merge_request ? 'create-mr' : 'create-branch' +- value = can_create_merge_request ? 'Create a merge request' : 'Create a branch' +  - if can?(current_user, :push_code, @project)    .create-mr-dropdown-wrap{ data: { can_create_path: can_create_branch_project_issue_path(@project, @issue), create_mr_path: create_merge_request_project_issue_path(@project, @issue), create_branch_path: project_branches_path(@project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid) } }      .btn-group.unavailable @@ -6,20 +10,21 @@          %span.text            Checking branch availability…      .btn-group.available.hide -      %input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: 'Create a merge request', data: { action: 'create-mr' } } +      %input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: value, data: { action: data_action } }        %button.btn.btn-inverted.dropdown-toggle.btn-inverted.btn-success.js-dropdown-toggle{ type: 'button', data: { 'dropdown-trigger' => '#create-merge-request-dropdown' } }          = icon('caret-down')        %ul#create-merge-request-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } } -        %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } } -          .menu-item -            .icon-container -              = icon('check') -            .description -              %strong Create a merge request -              %span -                Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'. -        %li.divider.droplab-item-ignore -        %li{ role: 'button', data: {  value: 'create-branch', 'text' => 'Create a branch' } } +        - if can_create_merge_request +          %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } } +            .menu-item +              .icon-container +                = icon('check') +              .description +                %strong Create a merge request +                %span +                  Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'. +          %li.divider.droplab-item-ignore +        %li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: {  value: 'create-branch', 'text' => 'Create a branch' } }            .menu-item              .icon-container                = icon('check') diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml new file mode 100644 index 00000000000..1b7d878c38c --- /dev/null +++ b/app/views/projects/issues/edit.html.haml @@ -0,0 +1,7 @@ +- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues" + +%h3.page-title +  Edit Issue ##{@issue.iid} +%hr + += render "form" diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index d820fc40ad7..5a12607afa4 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -4,8 +4,10 @@    .sidebar-container      .blocks-container        .block -        %strong +        %strong.prepend-top-10            = @build.name +        - if can?(current_user, :update_build, @build) && @build.retryable? +          = link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post          %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' }            = icon('angle-double-right') @@ -48,7 +50,7 @@            - if @build.trigger_variables.any?              %p -              %button.btn.group.btn-group-justified.reveal-variables Reveal Variables +              %button.btn.group.btn-group-justified.js-reveal-variables Reveal Variables              %dl.js-build-variables.trigger-build-variables.hide                - @build.trigger_variables.each do |trigger_variable| @@ -89,7 +91,7 @@          - builds.select{|build| build.status == build_status}.each do |build|            .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }              = link_to project_job_path(@project, build) do -              = icon('arrow-right') +              = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')                %span{ class: "ci-status-icon-#{build.status}" }                  = ci_icon_for_status(build.status)                %span @@ -98,4 +100,5 @@                  - else                    = build.id                - if build.retried? -                %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } +                %span.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } +                  = sprite_icon('retry', size:16, css_class: 'icon-retry') diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index cb723fe6a18..72d5c4961ec 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -34,7 +34,7 @@              %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }                = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'              %li{ class: merge_request_button_visibility(@merge_request, false) } -              = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' +              = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'        - if can_update_merge_request          = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 6b8dcb3e60b..8da2243adef 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -13,8 +13,6 @@  - if @project.merge_requests.exists?    %div{ class: container_class } -    - if show_auto_devops_callout?(@project) -      = render 'shared/auto_devops_callout'      .top-area        = render 'shared/issuable/nav', type: :merge_requests        .nav-controls diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index a5153df1159..9fc297ab7f6 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -23,14 +23,18 @@          = milestone_date_range(@milestone)      .milestone-buttons        - if can?(current_user, :admin_milestone, @project) +        = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do +          Edit + +        - if @project.group +          = link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "Promoting this milestone will make it available for all projects inside the group. Existing project milestones with the same name will be merged. Are you sure?", toggle: "tooltip" }, method: :post do +            Promote +          - if @milestone.active?            = link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"          - else            = link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" -        = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do -          Edit -          = link_to project_milestone_path(@project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do            Delete @@ -40,6 +44,7 @@    .detail-page-description.milestone-detail      %h2.title        = markdown_field(@milestone, :title) +      %div        - if @milestone.description.present?          .description diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index cc41b908946..0a7880ce4cd 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -14,114 +14,88 @@      .col-lg-3.profile-settings-sidebar        %h4.prepend-top-0          New project -      - if import_sources_enabled? -        %p -          A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}. -        %p -          All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings. +      %p +        A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}. +      %p +        All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings.      .col-lg-9.js-toggle-container -      = form_for @project, html: { class: 'new_project' } do |f| -        .create-project-options -          .first-column +      %ul.nav-links.gitlab-tabs{ role: 'tablist' } +        %li.active{ role: 'presentation' } +          %a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' } +            %span.hidden-xs Blank project +            %span.visible-xs Blank +        %li{ role: 'presentation' } +          %a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' } +            %span.hidden-xs Create from template +            %span.visible-xs Template +        %li{ role: 'presentation' } +          %a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' } +            %span.hidden-xs Import project +            %span.visible-xs Import + +      .tab-content.gitlab-tab-content +        .tab-pane.active{ id: 'blank-project-pane', role: 'tabpanel' } +          = form_for @project, html: { class: 'new_project' } do |f| +            = render 'new_project_fields', f: f, project_name_id: "blank-project-name" + +        .tab-pane.no-padding{ id: 'create-from-template-pane', role: 'tabpanel' } +          = form_for @project, html: { class: 'new_project' } do |f|              .project-template                .form-group -                = f.label :template_project, class: 'label-light' do -                  Create from template -                  = link_to icon('question-circle'), help_page_path("gitlab-basics/create-project"), target: '_blank', aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'}                  %div                    = render 'project_templates', f: f -          - if import_sources_enabled? -            .second-column -              .project-import -                .form-group.clearfix -                  = f.label :visibility_level, class: 'label-light' do #the label here seems wrong -                    Import project from -                  .col-sm-12.import-buttons -                    %div -                      - if github_import_enabled? -                        = link_to new_import_github_path, class: 'btn import_github' do -                          = icon('github', text: 'GitHub') -                    %div -                      - if bitbucket_import_enabled? -                        = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do -                          = icon('bitbucket', text: 'Bitbucket') -                        - unless bitbucket_import_configured? -                          = render 'bitbucket_import_modal' -                    %div -                      - if gitlab_import_enabled? -                        = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do -                          = icon('gitlab', text: 'GitLab.com') -                        - unless gitlab_import_configured? -                          = render 'gitlab_import_modal' -                    %div -                      - if google_code_import_enabled? -                        = link_to new_import_google_code_path, class: 'btn import_google_code' do -                          = icon('google', text: 'Google Code') -                    %div -                      - if fogbugz_import_enabled? -                        = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do -                          = icon('bug', text: 'Fogbugz') -                    %div -                      - if gitea_import_enabled? -                        = link_to new_import_gitea_url, class: 'btn import_gitea' do -                          = custom_icon('go_logo') -                          Gitea -                    %div -                      - if git_import_enabled? -                        %button.btn.js-toggle-button.import_git{ type: "button" } -                          = icon('git', text: 'Repo by URL') -                    - if gitlab_project_import_enabled? -                      .import_gitlab_project.has-tooltip{ data: { container: 'body' } } -                        = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do -                          = icon('gitlab', text: 'GitLab export') - -        .row -          .col-lg-12 -            .js-toggle-content.hide -              %hr -                = render "shared/import_form", f: f -        %hr - -        .row -          .form-group.col-xs-12.col-sm-6 -            = f.label :namespace_id, class: 'label-light' do -              %span -                Project path -            .form-group -              .input-group -                - if current_user.can_select_namespace? -                  .input-group-addon -                    = root_url -                  = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1} - -                - else -                  .input-group-addon.static-namespace -                    #{root_url}#{current_user.username}/ -                  = f.hidden_field :namespace_id, value: current_user.namespace_id -          .form-group.col-xs-12.col-sm-6.project-path -            = f.label :path, class: 'label-light' do -              %span -                Project name -            = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true -        - if current_user.can_create_group? -          .help-block -            Want to house several dependent projects under the same namespace? -            = link_to "Create a group", new_group_path - -        .form-group -          = f.label :description, class: 'label-light' do -            Project description -            %span.light (optional) -          = f.text_area :description, placeholder: 'Description format',  class: "form-control", rows: 3, maxlength: 250 - -        .form-group.visibility-level-setting -          = f.label :visibility_level, class: 'label-light' do -            Visibility Level -            = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' } -          = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false -        = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 -        = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' +        .tab-pane.import-project-pane{ id: 'import-project-pane', role: 'tabpanel' } +          = form_for @project, html: { class: 'new_project' } do |f| +            - if import_sources_enabled? +              .project-import.row +                .col-sm-12 +                  .form-group.import-btn-container.clearfix +                    = f.label :visibility_level, class: 'label-light' do #the label here seems wrong +                      Import project from +                    .import-buttons +                      - if gitlab_project_import_enabled? +                        .import_gitlab_project.has-tooltip{ data: { container: 'body' } } +                          = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do +                            = icon('gitlab', text: 'GitLab export') +                      %div +                        - if github_import_enabled? +                          = link_to new_import_github_path, class: 'btn import_github' do +                            = icon('github', text: 'GitHub') +                      %div +                        - if bitbucket_import_enabled? +                          = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do +                            = icon('bitbucket', text: 'Bitbucket') +                          - unless bitbucket_import_configured? +                            = render 'bitbucket_import_modal' +                      %div +                        - if gitlab_import_enabled? +                          = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do +                            = icon('gitlab', text: 'GitLab.com') +                          - unless gitlab_import_configured? +                            = render 'gitlab_import_modal' +                      %div +                        - if google_code_import_enabled? +                          = link_to new_import_google_code_path, class: 'btn import_google_code' do +                            = icon('google', text: 'Google Code') +                      %div +                        - if fogbugz_import_enabled? +                          = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do +                            = icon('bug', text: 'Fogbugz') +                      %div +                        - if gitea_import_enabled? +                          = link_to new_import_gitea_url, class: 'btn import_gitea' do +                            = custom_icon('go_logo') +                            Gitea +                      %div +                        - if git_import_enabled? +                          %button.btn.js-toggle-button.import_git{ type: "button" } +                            = icon('git', text: 'Repo by URL') +                .col-lg-12 +                  .js-toggle-content.hide.toggle-import-form +                    %hr +                      = render "shared/import_form", f: f +                      = render 'new_project_fields', f: f, project_name_id: "import-url-name"  .save-project-loader.hide    .center diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index a10a7c23924..f8627a3818b 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -2,8 +2,6 @@  - page_title "Pipelines"  %div{ 'class' => container_class } -  - if show_auto_devops_callout?(@project) -    = render 'shared/auto_devops_callout'    #pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),      "help-page-path" => help_page_path('ci/quick_start/README'),      "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 25153fd0b6f..fd5d3ec56da 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -17,14 +17,14 @@          %i Owners      .light        - if can?(current_user, :admin_project_member, @project) -        %ul.nav-links.project-member-tabs{ role: 'tablist' } +        %ul.nav-links.gitlab-tabs{ role: 'tablist' }            %li.active{ role: 'presentation' }              %a{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member            - if @project.allowed_to_share_with_group?              %li{ role: 'presentation' }                %a{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group -        .tab-content.project-member-tab-content +        .tab-content.gitlab-tab-content            .tab-pane.active{ id: 'add-member-pane', role: 'tabpanel' }              = render 'projects/project_members/new_project_member', tab_title: 'Add member'            .tab-pane{ id: 'share-with-group-pane', role: 'tabpanel' } diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml index 6a47cbdf724..ba7d98228c3 100644 --- a/app/views/projects/protected_branches/shared/_index.html.haml +++ b/app/views/projects/protected_branches/shared/_index.html.haml @@ -1,6 +1,6 @@  - expanded = Rails.env.test? -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) }    .settings-header      %h4        Protected Branches @@ -8,7 +8,7 @@        = expanded ? 'Collapse' : 'Expand'      %p        Keep stable branches secure and force developers to use merge requests. -  .settings-content.no-animate{ class: ('expanded' if expanded) } +  .settings-content      %p        By default, protected branches are designed to:        %ul diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml index c07bd454ff6..e764a37bbd7 100644 --- a/app/views/projects/protected_tags/shared/_index.html.haml +++ b/app/views/projects/protected_tags/shared/_index.html.haml @@ -1,6 +1,6 @@  - expanded = Rails.env.test? -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) }    .settings-header      %h4        Protected Tags @@ -8,7 +8,7 @@        = expanded ? 'Collapse' : 'Expand'      %p        Limit access to creating and updating tags. -  .settings-content.no-animate{ class: ('expanded' if expanded) } +  .settings-content      %p        By default, protected tags are designed to:        %ul diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 62455d0d40d..664a4554692 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -4,7 +4,7 @@  - expanded = Rails.env.test? -%section.settings#js-general-pipeline-settings +%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if expanded) }    .settings-header      %h4        General pipelines settings @@ -12,10 +12,10 @@        = expanded ? 'Collapse' : 'Expand'      %p        Update your CI/CD configuration, like job timeout or Auto DevOps. -  .settings-content.no-animate{ class: ('expanded' if expanded) } +  .settings-content      = render 'projects/pipelines_settings/show' -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) }    .settings-header      %h4        Runners settings @@ -23,10 +23,10 @@        = expanded ? 'Collapse' : 'Expand'      %p        Register and see your runners for this project. -  .settings-content.no-animate{ class: ('expanded' if expanded) } +  .settings-content      = render 'projects/runners/index' -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) }    .settings-header      %h4        Secret variables @@ -35,10 +35,10 @@        = expanded ? 'Collapse' : 'Expand'      %p        = render "ci/variables/content" -  .settings-content.no-animate{ class: ('expanded' if expanded) } +  .settings-content      = render 'ci/variables/index' -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) }    .settings-header      %h4        Pipeline triggers @@ -48,5 +48,5 @@        Triggers can force a specific branch or tag to get rebuilt with an API call.  These tokens will        impersonate their associated user including their access to projects and their project        permissions. -  .settings-content.no-animate{ class: ('expanded' if expanded) } +  .settings-content      = render 'projects/triggers/index' diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 1927216e191..467f19b4c56 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -7,7 +7,7 @@      - if protected_tag?(@project, tag)        %span.label.label-success.prepend-left-4 -        protected +        = s_('TagsPage|protected')      - if tag.message.present?          @@ -18,7 +18,7 @@          = render 'projects/branches/commit', commit: commit, project: @project      - else        %p -        Cant find HEAD commit for this tag +        = s_("TagsPage|Can't find HEAD commit for this tag")      - if release && release.description.present?        .description.prepend-top-default          .wiki @@ -28,9 +28,9 @@      = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]      - if can?(current_user, :push_code, @project) -      = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do +      = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do          = icon("pencil")      - if can?(current_user, :admin_project, @project) -      = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do +      = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do          = icon("trash-o") diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 27d58d4c0e8..fd3b8c01b83 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,16 +1,16 @@  - @no_container = true  - @sort ||= sort_value_recently_updated -- page_title "Tags" +- page_title _('TagsPage|Tags')  - add_to_breadcrumbs("Repository", project_tree_path(@project))  .flex-list{ class: container_class }    .top-area.adjust      .nav-text.row-main-content -      Tags give the ability to mark specific points in history as being important +      = s_('TagsPage|Tags give the ability to mark specific points in history as being important')      .nav-controls.row-fixed-content        = form_tag(filter_tags_path, method: :get) do -        = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false } +        = search_field_tag :search, params[:search], { placeholder: s_('TagsPage|Filter by tag name'), id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }        .dropdown          %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} } @@ -19,13 +19,13 @@            = icon('chevron-down')          %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable            %li.dropdown-header -            Sort by +            = s_('TagsPage|Sort by')            - tags_sort_options_hash.each do |value, title|              %li                = link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value)        - if can?(current_user, :push_code, @project)          = link_to new_project_tag_path(@project), class: 'btn btn-create new-tag-btn' do -          New tag +          = s_('TagsPage|New tag')    .tags      - if @tags.any? @@ -36,9 +36,9 @@      - else        .nothing-here-block -        Repository has no tags yet. +        = s_('TagsPage|Repository has no tags yet.')          %br          %small -          Use git tag command to add a new one: +          = s_('TagsPage|Use git tag command to add a new one:')            %br            %span.monospace git tag -a v1.4 -m 'version 1.4' diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 521b4d927bc..3e99e0e8234 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -1,4 +1,4 @@ -- page_title "New Tag" +- page_title s_('TagsPage|New Tag')  - default_ref = params[:ref] || @project.default_branch  - if @error @@ -7,7 +7,7 @@      = @error  %h3.page-title -  New Tag +  = s_('TagsPage|New Tag')  %hr  = form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal common-note-form tag-form js-quick-submit js-requires-input" do @@ -23,20 +23,23 @@          = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do            .text-left.dropdown-toggle-text= default_ref          = render 'shared/ref_dropdown', dropdown_class: 'wide' -      .help-block Existing branch name, tag, or commit SHA +      .help-block +        = s_('TagsPage|Existing branch name, tag, or commit SHA')    .form-group      = label_tag :message, nil, class: 'control-label'      .col-sm-10        = text_area_tag :message, @message, required: false, tabindex: 3, class: 'form-control', rows: 5 -      .help-block Optionally, add a message to the tag. +      .help-block +        = s_('TagsPage|Optionally, add a message to the tag.')    %hr    .form-group      = label_tag :release_description, 'Release notes', class: 'control-label'      .col-sm-10        = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do -        = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here...", current_text: @release_description +        = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here...'), current_text: @release_description          = render 'shared/notes/hints' -      .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. +      .help-block +        = s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.')    .form-actions      = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3      = link_to 'Cancel', project_tags_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 43aa2b27af6..dfe2c37ed8e 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -1,7 +1,7 @@  - @no_container = true -- add_to_breadcrumbs "Tags", project_tags_path(@project) +- add_to_breadcrumbs s_('TagsPage|Tags'), project_tags_path(@project)  - breadcrumb_title @tag.name -- page_title @tag.name, "Tags" +- page_title @tag.name, s_('TagsPage|Tags')  %div{ class: container_class }    .top-area.multi-line @@ -12,25 +12,25 @@            = @tag.name          - if protected_tag?(@project, @tag)            %span.label.label-success -            protected +            = s_('TagsPage|protected')        - if @commit          = render 'projects/branches/commit', commit: @commit, project: @project        - else -        Cant find HEAD commit for this tag +        = s_("TagsPage|Can't find HEAD commit for this tag")      .nav-controls.controls-flex        - if can?(current_user, :push_code, @project) -        = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Edit release notes' do +        = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do            = icon("pencil") -      = link_to project_tree_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse files' do +      = link_to project_tree_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Browse files') do          = icon('files-o') -      = link_to project_commits_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse commits' do +      = link_to project_commits_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Browse commits') do          = icon('history')        .btn-container.controls-item          = render 'projects/buttons/download', project: @project, ref: @tag.name        - if can?(current_user, :admin_project, @project)          .btn-container.controls-item-full -          = link_to project_tag_path(@project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do +          = link_to project_tag_path(@project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: @tag.name } } do              %i.fa.fa-trash-o      - if @tag.message.present? @@ -43,4 +43,4 @@          .wiki            = markdown_field(@release, :description)      - else -      This tag has no release notes. +      = s_('TagsPage|This tag has no release notes.') diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 6cddc38d11a..c02f7ee37ed 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -1,13 +1,15 @@  .tree-ref-container    .tree-ref-holder -    = render 'shared/ref_switcher', destination: 'tree', path: @path +    = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true -  - unless show_new_repo? +  - if show_new_repo? && can_push_branch?(@project, @ref) +    .js-new-dropdown +  - else      = render 'projects/tree/old_tree_header'  .tree-controls    - if show_new_repo? -    = render 'shared/repo/editable_mode' +    .editable-mode    - else      = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn' diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 0cc6674842a..745a6040488 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -12,7 +12,5 @@      = webpack_bundle_tag 'repo'  %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } -  - if show_auto_devops_callout?(@project) && !show_new_repo? -    = render 'shared/auto_devops_callout'    = render 'projects/last_push'    = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id) diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml index 911e1339541..d6e568bac94 100644 --- a/app/views/projects/wikis/empty.html.haml +++ b/app/views/projects/wikis/empty.html.haml @@ -1,6 +1,6 @@  - page_title _("Wiki") -%h3.page-title= _("Wiki|Empty page") +%h3.page-title= s_("Wiki|Empty page")  %hr  .error_message    = s_("WikiEmptyPageError|You are not allowed to create wiki pages") diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml index 7c633175a06..934d65e8b42 100644 --- a/app/views/shared/_auto_devops_callout.html.haml +++ b/app/views/shared/_auto_devops_callout.html.haml @@ -1,15 +1,16 @@ -.user-callout{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } -  .bordered-box.landing.content-block -    %button.btn.btn-default.close.js-close-callout{ type: 'button', -      'aria-label' => 'Dismiss Auto DevOps box' } -      = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true') -    .svg-container -      = custom_icon('icon_autodevops') -    .user-callout-copy -      %h4= s_('AutoDevOps|Auto DevOps (Beta)') -      %p= s_('AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') -      %p -        - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') -        = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } +.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } +  .banner-graphic +    = custom_icon('icon_autodevops') -      = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn btn-primary js-close-callout' +  .prepend-top-10.prepend-left-10.append-bottom-10 +    %h5= s_('AutoDevOps|Auto DevOps (Beta)') +    %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') +    %p +      - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') +      = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } +    .prepend-top-10 +      = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout' + +  %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button', +  'aria-label' => 'Dismiss Auto DevOps box' } +    = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true') diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml index dff847159d3..901a177323b 100644 --- a/app/views/shared/_mini_pipeline_graph.html.haml +++ b/app/views/shared/_mini_pipeline_graph.html.haml @@ -7,7 +7,7 @@        .stage-container.dropdown{ class: klass }          %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } } -          = custom_icon(icon_status) +          = sprite_icon(icon_status)            = icon('caret-down')          %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 7ad743b3b81..6356e9f92cb 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,3 +1,4 @@ +- show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project)  - dropdown_toggle_text = @ref || @project.default_branch  = form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do    = hidden_field_tag :destination, destination @@ -6,9 +7,21 @@    - @options && @options.each do |key, value|      = hidden_field_tag key, value, id: nil    .dropdown -    = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" } -    .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } -      = dropdown_title _("Switch branch/tag") -      = dropdown_filter _("Search branches and tags") -      = dropdown_content -      = dropdown_loading +    = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" } +    .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } +      .dropdown-page-one +        = dropdown_title _("Switch branch/tag") +        = dropdown_filter _("Search branches and tags") +        = dropdown_content +        = dropdown_loading +        - if show_new_branch_form +          = dropdown_footer do +            %ul.dropdown-footer-list +              %li +                %a.dropdown-toggle-page{ href: "#" } +                  Create new branch +      - if show_new_branch_form +        .dropdown-page-two +          = dropdown_title("Create new branch", options: { back: true }) +          = dropdown_content do +            .js-new-branch-dropdown diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml index 1f540bdaf93..dfc0f9be321 100644 --- a/app/views/shared/boards/components/sidebar/_labels.html.haml +++ b/app/views/shared/boards/components/sidebar/_labels.html.haml @@ -25,7 +25,7 @@              show_any: "true",              project_id: @project&.try(:id),              labels: labels_filter_path(false), -            namespace_path: @project.try(:namespace).try(:full_path), +            namespace_path: @namespace_path,              project_path: @project.try(:path) },            ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }            %span.dropdown-toggle-text diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml index 760370a6984..8e6747ca740 100644 --- a/app/views/shared/groups/_dropdown.html.haml +++ b/app/views/shared/groups/_dropdown.html.haml @@ -1,18 +1,32 @@ -.dropdown.inline.js-group-filter-dropdown-wrap +- show_archive_options = local_assigns.fetch(:show_archive_options, false) +- if @sort.present? +  - default_sort_by = @sort +- else +  - if params[:sort] +    - default_sort_by = params[:sort] +  - else +    - default_sort_by = sort_value_recently_created + +.dropdown.inline.js-group-filter-dropdown-wrap.append-right-10    %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }      %span.dropdown-label -      - if @sort.present? -        = sort_options_hash[@sort] -      - else -        = sort_title_recently_created +      = sort_options_hash[default_sort_by]      = icon('chevron-down') -  %ul.dropdown-menu.dropdown-menu-align-right -    %li -      = link_to filter_groups_path(sort: sort_value_recently_created) do -        = sort_title_recently_created -      = link_to filter_groups_path(sort: sort_value_oldest_created) do -        = sort_title_oldest_created -      = link_to filter_groups_path(sort: sort_value_recently_updated) do -        = sort_title_recently_updated -      = link_to filter_groups_path(sort: sort_value_oldest_updated) do -        = sort_title_oldest_updated +  %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable +    %li.dropdown-header +      = _("Sort by") +    - groups_sort_options_hash.each do |value, title| +      %li.js-filter-sort-order +        = link_to filter_groups_path(sort: value), class: ("is-active" if default_sort_by == value) do +          = title +    - if show_archive_options +      %li.divider +      %li.js-filter-archived-projects +        = link_to group_children_path(@group, archived: nil), class: ("is-active" unless params[:archived].present?) do +          Hide archived projects +      %li.js-filter-archived-projects +        = link_to group_children_path(@group, archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do +          Show archived projects +      %li.js-filter-archived-projects +        = link_to group_children_path(@group, archived: 'only'), class: ("is-active" if params[:archived] == 'only') do +          Show archived projects only diff --git a/app/views/shared/groups/_empty_state.html.haml b/app/views/shared/groups/_empty_state.html.haml new file mode 100644 index 00000000000..13bb4baee3f --- /dev/null +++ b/app/views/shared/groups/_empty_state.html.haml @@ -0,0 +1,7 @@ +.groups-empty-state +  = custom_icon("icon_empty_groups") + +  .text-content +    %h4= s_("GroupsEmptyState|A group is a collection of several projects.") +    %p= s_("GroupsEmptyState|If you organize your projects under a group, it works like a folder.") +    %p= s_("GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group.") diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index b361ec86ced..059dd24be6d 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -11,7 +11,7 @@          = link_to edit_group_path(group), class: "btn" do            = icon('cogs') -      = link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: 'Leave this group' do +      = link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: s_("GroupsTree|Leave this group") do          = icon('sign-out')    .stats @@ -28,7 +28,7 @@    .avatar-container.s40      = link_to group do -      = image_tag group_icon(group), class: "avatar s40 hidden-xs" +      = group_icon(group, class: "avatar s40 hidden-xs")    .title      = link_to group_name, group, class: 'group-name' diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml index 427595c47a5..aec8ecd1714 100644 --- a/app/views/shared/groups/_list.html.haml +++ b/app/views/shared/groups/_list.html.haml @@ -3,4 +3,4 @@      - groups.each_with_index do |group, i|        = render "shared/groups/group", group: group  - else -  .nothing-here-block No groups found +  .nothing-here-block= s_("GroupsEmptyState|No groups found") diff --git a/app/views/shared/groups/_search_form.html.haml b/app/views/shared/groups/_search_form.html.haml index ad7a7faedf1..3f91263089a 100644 --- a/app/views/shared/groups/_search_form.html.haml +++ b/app/views/shared/groups/_search_form.html.haml @@ -1,2 +1,2 @@ -= form_tag request.path, method: :get, class: 'group-filter-form', id: 'group-filter-form' do |f| -  = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name...', class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2" += form_tag request.path, method: :get, class: 'group-filter-form append-right-10', id: 'group-filter-form' do |f| +  = search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Filter by name...'), class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2" diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml index af6a499fadb..c80b179d525 100644 --- a/app/views/shared/hook_logs/_content.html.haml +++ b/app/views/shared/hook_logs/_content.html.haml @@ -11,7 +11,7 @@        = hook_log.trigger.singularize.titleize  %p    %strong Elapsed time: -  #{number_with_precision(hook_log.execution_duration, precision: 2)} ms +  #{number_with_precision(hook_log.execution_duration, precision: 2)} sec  %p    %strong Request time:    = time_ago_with_tooltip(hook_log.created_at) diff --git a/app/views/shared/icons/_express.svg b/app/views/shared/icons/_express.svg index f2c94319f19..a51e81e5568 100644 --- a/app/views/shared/icons/_express.svg +++ b/app/views/shared/icons/_express.svg @@ -1,6 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="27" height="32" viewBox="0 0 27 32" class="btn-template-icon icon-node-express"> -  <g fill="none" fill-rule="evenodd" transform="translate(-3)"> -    <rect width="32" height="32"/> -    <path fill="#353535" d="M4.19170065,16.2667139 C4.23142421,18.3323387 4.47969269,20.2489714 4.93651356,22.0166696 C5.39333443,23.7843677 6.09841693,25.3236323 7.05178222,26.6345096 C8.00514751,27.9453869 9.23655921,28.9781838 10.7460543,29.7329313 C12.2555493,30.4876788 14.1026668,30.8650469 16.2874623,30.8650469 C19.5050701,30.8650469 22.1764391,30.0209341 24.3016492,28.3326831 C26.4268593,26.644432 27.7476477,24.1120935 28.2640539,20.7355914 L29.4557545,20.7355914 C29.0187954,24.3107112 27.6086304,27.0813875 25.2252172,29.0477034 C22.841804,31.0140194 19.9023051,31.9971626 16.4066324,31.9971626 C14.0232191,32.0368861 11.9874175,31.659518 10.2991665,30.8650469 C8.61091547,30.0705759 7.23054269,28.9484023 6.15800673,27.4984926 C5.08547078,26.0485829 4.29101162,24.3404957 3.77460543,22.3741798 C3.25819923,20.4078639 3,18.2926164 3,16.0283738 C3,13.4860664 3.3773681,11.2218578 4.13211562,9.23568007 C4.88686314,7.24950238 5.87993709,5.57120741 7.11136726,4.20074481 C8.34279742,2.8302822 9.77282391,1.78755456 11.4014896,1.07253059 C13.0301553,0.357506621 14.6985195,0 16.4066324,0 C18.7900456,0 20.8457087,0.456814016 22.5736832,1.37045575 C24.3016578,2.28409749 25.7118228,3.4956477 26.8042206,5.00514275 C27.8966183,6.51463779 28.6910775,8.24258646 29.1876219,10.1890406 C29.6841663,12.1354947 29.8927118,14.1613656 29.8132647,16.2667139 L4.19170065,16.2667139 Z M28.6215641,15.0750133 C28.6215641,13.2080062 28.3633648,11.4304039 27.8469586,9.74215285 C27.3305524,8.05390181 26.5658855,6.57422163 25.5529349,5.30306791 C24.5399843,4.03191419 23.2787803,3.0289095 21.7692853,2.29402376 C20.2597903,1.55913801 18.5119801,1.19170065 16.5258024,1.19170065 C14.8574132,1.19170065 13.2982871,1.50948432 11.8483774,2.14506118 C10.3984676,2.78063804 9.12733299,3.70419681 8.03493526,4.9157652 C6.94253754,6.12733359 6.05870172,7.58715229 5.38340131,9.2952651 C4.70810089,11.0033779 4.31087132,12.9299414 4.19170065,15.0750133 L28.6215641,15.0750133 Z"/> -  </g> -</svg> +<svg xmlns="http://www.w3.org/2000/svg" width="27" height="32" viewBox="0 0 27 32" class="btn-template-icon icon-node-express"><g fill="none" fill-rule="evenodd"><path d="M-3 0h32v32H-3z"/><path fill="#353535" d="M1.192 16.267c.04 2.065.288 3.982.745 5.75.456 1.767 1.16 3.307 2.115 4.618.953 1.31 2.185 2.343 3.694 3.098 1.51.755 3.357 1.132 5.54 1.132 3.22 0 5.89-.844 8.016-2.532 2.125-1.69 3.446-4.22 3.962-7.597h1.192c-.437 3.575-1.847 6.345-4.23 8.312-2.384 1.966-5.324 2.95-8.82 2.95-2.383.04-4.42-.338-6.107-1.133-1.69-.794-3.07-1.917-4.142-3.367-1.073-1.45-1.867-3.158-2.383-5.124C.258 20.408 0 18.294 0 16.028c0-2.542.377-4.806 1.132-6.792C1.887 7.25 2.88 5.57 4.112 4.2 5.34 2.83 6.77 1.79 8.4 1.074 10.03.358 11.698 0 13.406 0c2.383 0 4.44.457 6.167 1.37 1.728.914 3.138 2.126 4.23 3.635 1.093 1.51 1.887 3.238 2.384 5.184.496 1.945.705 3.97.625 6.077H1.193zm24.43-1.192c0-1.867-.26-3.645-.775-5.333-.516-1.688-1.28-3.168-2.294-4.44-1.013-1.27-2.274-2.273-3.784-3.008-1.51-.735-3.258-1.102-5.244-1.102-1.67 0-3.228.317-4.678.953-1.45.636-2.72 1.56-3.813 2.77-1.092 1.212-1.976 2.672-2.652 4.38-.675 1.708-1.072 3.635-1.19 5.78h24.43z"/></g></svg> diff --git a/app/views/shared/icons/_icon_autodevops.svg b/app/views/shared/icons/_icon_autodevops.svg index 807ff27bb67..7e47c084bde 100644 --- a/app/views/shared/icons/_icon_autodevops.svg +++ b/app/views/shared/icons/_icon_autodevops.svg @@ -1,4 +1,4 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="189" height="179" viewBox="0 0 189 179"> +<svg xmlns="http://www.w3.org/2000/svg" width="189" height="110" viewBox="0 0 189 179">    <g fill="none" fill-rule="evenodd">      <path fill="#FFFFFF" fill-rule="nonzero" d="M110.160166,47.6956996 L160.160166,47.6956996 C165.683013,47.6956996 170.160166,52.1728521 170.160166,57.6956996 L170.160166,117.6957 C170.160166,123.218547 165.683013,127.6957 160.160166,127.6957 L110.160166,127.6957 C104.637318,127.6957 100.160166,123.218547 100.160166,117.6957 L100.160166,57.6956996 C100.160166,52.1728521 104.637318,47.6956996 110.160166,47.6956996 Z" transform="rotate(10 135.16 87.696)"/>      <path fill="#EEEEEE" fill-rule="nonzero" d="M110.160166,51.6956996 C106.846457,51.6956996 104.160166,54.3819911 104.160166,57.6956996 L104.160166,117.6957 C104.160166,121.009408 106.846457,123.6957 110.160166,123.6957 L160.160166,123.6957 C163.473874,123.6957 166.160166,121.009408 166.160166,117.6957 L166.160166,57.6956996 C166.160166,54.3819911 163.473874,51.6956996 160.160166,51.6956996 L110.160166,51.6956996 Z M110.160166,47.6956996 L160.160166,47.6956996 C165.683013,47.6956996 170.160166,52.1728521 170.160166,57.6956996 L170.160166,117.6957 C170.160166,123.218547 165.683013,127.6957 160.160166,127.6957 L110.160166,127.6957 C104.637318,127.6957 100.160166,123.218547 100.160166,117.6957 L100.160166,57.6956996 C100.160166,52.1728521 104.637318,47.6956996 110.160166,47.6956996 Z" transform="rotate(10 135.16 87.696)"/> diff --git a/app/views/shared/icons/_rails.svg b/app/views/shared/icons/_rails.svg index 0bb09a705df..852bd183cc7 100644 --- a/app/views/shared/icons/_rails.svg +++ b/app/views/shared/icons/_rails.svg @@ -1,6 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20" viewBox="0 0 32 20" class="btn-template-icon icon-rails"> -  <g fill="none" fill-rule="evenodd" transform="translate(0 -6)"> -    <rect width="32" height="32"/> -    <path fill="#C00" fill-rule="nonzero" d="M0.984615385,25.636044 C0.984615385,25.636044 1.40659341,21.4725275 4.36043956,16.5494505 C7.31428571,11.6263736 12.3498901,7.8989011 16.4430769,7.53318681 C24.5872527,6.71736264 31.9015385,14.0175824 31.9015385,14.0175824 C31.9015385,14.0175824 31.6624176,14.1863736 31.4092308,14.3973626 C23.4197802,8.48967033 18.5389011,11.2747253 17.0057143,12.0202198 C9.97274725,15.9446154 12.0967033,25.636044 12.0967033,25.636044 L0.984615385,25.636044 Z M24.1371429,8.32087912 C23.687033,8.13802198 23.2369231,7.96923077 22.7727473,7.81450549 L22.829011,6.88615385 C23.7151648,7.13934066 24.0668132,7.30813187 24.1934066,7.37846154 L24.1371429,8.32087912 Z M22.8008791,11.3028571 C23.250989,11.330989 23.7151648,11.3872527 24.1934066,11.4857143 L24.1371429,12.3578022 C23.672967,12.2593407 23.2087912,12.2030769 22.7446154,12.189011 L22.8008791,11.3028571 Z M17.5964835,6.91428571 C17.1885714,6.91428571 16.7806593,6.92835165 16.3727473,6.97054945 L16.1054945,6.14065934 C16.5696703,6.0843956 17.0197802,6.05626374 17.4558242,6.05626374 L17.7371429,6.91428571 C17.6949451,6.91428571 17.6386813,6.91428571 17.5964835,6.91428571 Z M18.2716484,12.0905495 C18.6232967,11.9358242 19.0312088,11.7810989 19.5094505,11.6404396 L19.8189011,12.5687912 C19.410989,12.6953846 19.0030769,12.8641758 18.5951648,13.0610989 L18.2716484,12.0905495 Z M11.8857143,8.39120879 C11.52,8.57406593 11.1683516,8.78505495 10.8026374,9.01010989 L10.1556044,8.02549451 C10.5353846,7.80043956 10.9010989,7.60351648 11.2527473,7.42065934 L11.8857143,8.39120879 Z M14.7692308,14.7208791 C15.0224176,14.3973626 15.3178022,14.0738462 15.6413187,13.7784615 L16.2742857,14.7349451 C15.9648352,15.0584615 15.6835165,15.381978 15.4443956,15.7336264 L14.7692308,14.7208791 Z M12.7296703,19.2501099 C12.8421978,18.7437363 12.9687912,18.2232967 13.1516484,17.7028571 L14.1643956,18.5046154 C14.0237363,19.0531868 13.9252747,19.6017582 13.869011,20.1503297 L12.7296703,19.2501099 Z M6.56879121,12.5687912 C6.23120879,12.9204396 5.90769231,13.3002198 5.61230769,13.68 L4.52923077,12.7516484 C4.85274725,12.4 5.2043956,12.0483516 5.57010989,11.6967033 L6.56879121,12.5687912 Z M2.32087912,18.8562637 C2.09582418,19.3767033 1.80043956,20.0659341 1.61758242,20.5441758 L0,19.9534066 C0.140659341,19.5736264 0.436043956,18.8703297 0.703296703,18.2654945 L2.32087912,18.8562637 Z M12.5186813,22.8228571 L14.0378022,23.3714286 C14.1221978,24.0325275 14.2487912,24.6514286 14.3753846,25.2 L12.6874725,24.5951648 C12.6171429,24.1731868 12.5468132,23.5683516 12.5186813,22.8228571 Z"/> -  </g> -</svg> +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20" viewBox="0 0 32 20" class="btn-template-icon icon-rails"><g fill="none" fill-rule="evenodd"><path d="M0-6h32v32H0z"/><path fill="#c00" fill-rule="nonzero" d="M.985 19.636s.422-4.163 3.375-9.087c2.954-4.924 7.99-8.65 12.083-9.017 8.144-.816 15.46 6.485 15.46 6.485s-.24.168-.494.38C23.42 2.49 18.54 5.274 17.005 6.02c-7.033 3.925-4.91 13.616-4.91 13.616H.987zM24.137 2.32c-.45-.182-.9-.35-1.364-.505l.056-.93c.885.254 1.237.423 1.363.493l-.056.943zM22.8 5.304c.45.028.915.084 1.393.183l-.056.872c-.464-.1-.928-.155-1.392-.17l.056-.885zM17.597.913c-.407 0-.815.015-1.223.058l-.268-.83c.465-.056.915-.084 1.35-.084l.282.858h-.14zm.676 5.178c.35-.154.76-.31 1.237-.45l.31.93c-.41.125-.817.294-1.225.49l-.323-.97zm-6.386-3.7c-.366.184-.718.395-1.083.62l-.647-.985c.38-.225.745-.42 1.097-.604l.633.97zm2.883 6.33c.252-.323.548-.646.87-.942l.634.957c-.31.323-.59.647-.83 1L14.77 8.72zm-2.04 4.53c.112-.506.24-1.027.422-1.547l1.012.802c-.14.548-.24 1.097-.295 1.645l-1.14-.9zM6.57 6.57c-.34.35-.662.73-.958 1.11L4.53 6.752c.323-.352.674-.704 1.04-1.055l1 .872zm-4.25 6.286c-.224.52-.52 1.21-.702 1.688L0 13.954c.14-.38.436-1.084.703-1.69l1.618.592zm10.2 3.967l1.518.548c.084.663.21 1.28.337 1.83l-1.688-.605c-.07-.422-.14-1.027-.168-1.772z"/></g></svg> diff --git a/app/views/shared/icons/_spring.svg b/app/views/shared/icons/_spring.svg index 508349aa456..ccf18749029 100644 --- a/app/views/shared/icons/_spring.svg +++ b/app/views/shared/icons/_spring.svg @@ -1,6 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="btn-template-icon icon-java-spring"> -  <g fill="none" fill-rule="evenodd"> -    <rect width="32" height="32"/> -    <path fill="#70AD51" d="M5.46647617,27.9932117 C6.0517027,28.4658996 6.91159892,28.3777063 7.38425926,27.7914452 C7.85922261,27.2048452 7.76991326,26.3449044 7.18398981,25.8699411 C6.59874295,25.3956543 5.74015536,25.4869934 5.26383884,26.0722403 C4.81393367,26.6267596 4.87238621,27.4284565 5.37913494,27.9159868 L5.11431334,27.6818383 C1.97157151,24.7616933 0,20.5966301 0,15.9782542 C0,7.16842834 7.16775175,0 15.9796074,0 C20.4586065,0 24.5113565,1.8565519 27.4145869,4.8362365 C28.0749348,3.93840692 28.6466499,2.93435335 29.115524,1.82069284 C31.1513712,7.93770658 32.3482517,13.0811131 31.909824,17.1311567 C31.3178113,25.4044499 24.4017495,31.9585382 15.9796074,31.9585382 C12.0682639,31.9585382 8.48438805,30.5444735 5.7042963,28.2034861 L5.46647617,27.9932117 Z M29.0471888,23.0106888 C33.0546075,17.6737787 30.8211972,9.04527781 28.9612624,3.529749 C27.3029502,6.98304378 23.2217836,9.62375882 19.6981239,10.4613722 C16.3950312,11.2482417 13.4715032,10.6021021 10.4153644,11.7780085 C3.44517575,14.457289 3.55613585,22.7698242 7.39373146,24.6365249 C7.39711439,24.6392312 7.62444728,24.7616933 7.62174094,24.7576338 C7.62309411,24.7562806 13.2658211,23.6358542 16.3862356,22.4843049 C20.9450718,20.7996058 25.9524846,16.6494275 27.5986182,11.8273993 C26.723116,16.8415779 22.4179995,21.6669891 18.093262,23.8828081 C15.7908399,25.0648038 14.0005934,25.3279957 10.2123886,26.6385428 C9.74892722,26.798217 9.38492397,26.9538318 9.38492397,26.9538318 C10.3463526,26.7948341 11.301692,26.7420604 11.301692,26.7420604 C16.6954354,26.4869875 25.1087819,28.2582896 29.0471888,23.0106888 Z"/> -  </g> -</svg> +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="btn-template-icon icon-java-spring"><g fill="none" fill-rule="evenodd"><path d="M0 0h32v32H0z"/><path fill="#70AD51" d="M5.466 27.993c.586.473 1.446.385 1.918-.202.475-.585.386-1.445-.2-1.92-.585-.474-1.444-.383-1.92.202-.45.555-.392 1.356.115 1.844l-.266-.234C1.972 24.762 0 20.597 0 15.978 0 7.168 7.168 0 15.98 0c4.48 0 8.53 1.857 11.435 4.836.66-.898 1.232-1.902 1.7-3.015 2.036 6.118 3.233 11.26 2.795 15.31-.592 8.274-7.508 14.83-15.93 14.83-3.912 0-7.496-1.416-10.276-3.757l-.238-.21zm23.58-4.982c4.01-5.336 1.775-13.965-.085-19.48-1.657 3.453-5.738 6.094-9.262 6.93-3.303.788-6.226.142-9.283 1.318-6.97 2.68-6.86 10.992-3.02 12.86.002 0 .23.124.227.12 0-.002 5.644-1.122 8.764-2.274 4.56-1.684 9.566-5.835 11.213-10.657-.877 5.015-5.182 9.84-9.507 12.056-2.302 1.182-4.092 1.445-7.88 2.756-.464.158-.828.314-.828.314.96-.16 1.917-.212 1.917-.212 5.393-.255 13.807 1.516 17.745-3.73z"/></g></svg> diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index d3f0aa2d339..8442d7ff4a2 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,4 +1,3 @@ -- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder  - boards_page = controller.controller_name == 'boards'  .issues-filters diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml deleted file mode 100644 index d2b62557e03..00000000000 --- a/app/views/shared/issuable/_participants.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- participants_row = 7 -- participants_size = participants.size -- participants_extra = participants_size - participants_row -.block.participants -  .sidebar-collapsed-icon -    = icon('users') -    %span -      = participants.count -  .title.hide-collapsed -    = pluralize participants.count, "participant" -  .hide-collapsed.participants-list -    - participants.each do |participant| -      .participants-author.js-participants-author -        = link_to_member(@project, participant, name: false, size: 24, lazy_load: true) -  - if participants_extra > 0 -    .hide-collapsed.participants-more -      %a.js-participants-more{ href: "#", data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } } -        + #{participants_extra} more diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 161b1c9fd72..fabb17c7340 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -25,7 +25,6 @@                %ul.tokens-container.list-unstyled                  %li.input-token                    %input.form-control.filtered-search{ search_filter_input_options(type) } -              = icon('filter')              #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown                %ul{ data: { dropdown: true } }                  %li.filter-dropdown-item{ data: { action: 'submit' } } diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 7b7411b1e23..e0009a35b9f 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -123,17 +123,10 @@          %script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe          #js-lock-entry-point -      = render "shared/issuable/participants", participants: issuable.participants(current_user) +      .js-sidebar-participants-entry-point +        - if current_user -        - subscribed = issuable.subscribed?(current_user, @project) -        .block.light.subscription{ data: { url: toggle_subscription_path(issuable) } } -          .sidebar-collapsed-icon -            = icon('rss', 'aria-hidden': 'true') -          %span.issuable-header-text.hide-collapsed.pull-left -            Notifications -          - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' -          %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } -            %span= subscribed ? 'Unsubscribe' : 'Subscribe' +        .js-sidebar-subscriptions-entry-point        - project_ref = cross_project_reference(@project, issuable)        .block.project-reference diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index bcdad3c153a..5868c52566d 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -4,7 +4,7 @@  - dom_id = "group_member_#{group_link.id}"  %li.member.group_member{ id: dom_id }    %span.list-item-name -    = image_tag group_icon(group), class: "avatar s40", alt: '' +    = group_icon(group, class: "avatar s40", alt: '')      %strong        = link_to group.full_name, group_path(group)      .cgray diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 305e2542281..7ba8f9d4313 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -49,6 +49,13 @@            = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-xs btn-grouped" do              Edit            \ + +          - if @project.group +            = link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "Promoting this milestone will make it available for all projects inside the group. Existing project milestones with the same name will be merged. Are you sure?", toggle: "tooltip" }, method: :post do +              Promote +            = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped" +            = link_to project_milestone_path(milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove btn-grouped" do              Delete + diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index 80432a73e4e..3d917346f6b 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,5 +1,5 @@  - @sort ||= sort_value_latest_activity -.dropdown +.dropdown.js-project-filter-dropdown-wrap    - toggle_text = projects_sort_options_hash[@sort]    = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })    %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable diff --git a/app/views/shared/repo/_editable_mode.html.haml b/app/views/shared/repo/_editable_mode.html.haml deleted file mode 100644 index 73fdb8b523f..00000000000 --- a/app/views/shared/repo/_editable_mode.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -.editable-mode -  %repo-edit-button diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml index 87fa2007d16..5867ea58378 100644 --- a/app/views/shared/repo/_repo.html.haml +++ b/app/views/shared/repo/_repo.html.haml @@ -1,7 +1,12 @@ -#repo{ data: { url: content_url, +#repo{ data: { root: @path.empty?.to_s, +               root_url: project_tree_path(project), +               url: content_url, +               current_branch: @ref, +               ref: @commit.id,                 project_name: project.name, -               refs_url: refs_project_path(project, format: :json),                 project_url: project_path(project),                 project_id: project.id, +               new_merge_request_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '' }),                 can_commit: (!!can_push_branch?(project, @ref)).to_s, -               on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } } +               on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s, +               current_path: @path } } diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml index eff6c80d144..55799e10a46 100644 --- a/app/views/users/_groups.html.haml +++ b/app/views/users/_groups.html.haml @@ -2,4 +2,4 @@    - groups.each do |group|      = link_to group, class: 'profile-groups-avatars inline', title: group.name do        .avatar-container.s40 -        = image_tag group_icon(group), class: 'avatar group-avatar s40' +        = group_icon(group, class: 'avatar group-avatar s40') diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 6c3cd6ecefe..cc59f8660fd 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -4,6 +4,9 @@  - page_description @user.bio  - header_title     @user.name, user_path(@user)  - @no_container = true +- content_for :page_specific_javascripts do +  = webpack_bundle_tag 'common_d3' +  = webpack_bundle_tag 'users'  = content_for :meta_tags do    = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") diff --git a/app/workers/cluster_install_app_worker.rb b/app/workers/cluster_install_app_worker.rb new file mode 100644 index 00000000000..899aed904e4 --- /dev/null +++ b/app/workers/cluster_install_app_worker.rb @@ -0,0 +1,11 @@ +class ClusterInstallAppWorker +  include Sidekiq::Worker +  include ClusterQueue +  include ClusterApplications + +  def perform(app_name, app_id) +    find_application(app_name, app_id) do |app| +      Clusters::Applications::InstallService.new(app).execute +    end +  end +end diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb index 63300b58a25..b01f9708424 100644 --- a/app/workers/cluster_provision_worker.rb +++ b/app/workers/cluster_provision_worker.rb @@ -3,8 +3,10 @@ class ClusterProvisionWorker    include ClusterQueue    def perform(cluster_id) -    Gcp::Cluster.find_by_id(cluster_id).try do |cluster| -      Ci::ProvisionClusterService.new.execute(cluster) +    Clusters::Cluster.find_by_id(cluster_id).try do |cluster| +      cluster.provider.try do |provider| +        Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp? +      end      end    end  end diff --git a/app/workers/cluster_wait_for_app_installation_worker.rb b/app/workers/cluster_wait_for_app_installation_worker.rb new file mode 100644 index 00000000000..4bb8c293e5d --- /dev/null +++ b/app/workers/cluster_wait_for_app_installation_worker.rb @@ -0,0 +1,14 @@ +class ClusterWaitForAppInstallationWorker +  include Sidekiq::Worker +  include ClusterQueue +  include ClusterApplications + +  INTERVAL = 10.seconds +  TIMEOUT = 20.minutes + +  def perform(app_name, app_id) +    find_application(app_name, app_id) do |app| +      Clusters::Applications::CheckInstallationProgressService.new(app).execute +    end +  end +end diff --git a/app/workers/concerns/cluster_applications.rb b/app/workers/concerns/cluster_applications.rb new file mode 100644 index 00000000000..24ecaa0b52f --- /dev/null +++ b/app/workers/concerns/cluster_applications.rb @@ -0,0 +1,9 @@ +module ClusterApplications +  extend ActiveSupport::Concern + +  included do +    def find_application(app_name, id, &blk) +      Clusters::Cluster::APPLICATIONS[app_name].find(id).try(&blk) +    end +  end +end diff --git a/app/workers/concerns/project_start_import.rb b/app/workers/concerns/project_start_import.rb new file mode 100644 index 00000000000..0704ebbb0fd --- /dev/null +++ b/app/workers/concerns/project_start_import.rb @@ -0,0 +1,9 @@ +module ProjectStartImport +  def start(project) +    if project.import_started? && project.import_jid == self.jid +      return true +    end + +    project.import_start +  end +end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index cde5b45ad41..264706e3e23 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -4,6 +4,7 @@ class RepositoryForkWorker    include Sidekiq::Worker    include Gitlab::ShellAdapter    include DedicatedSidekiqQueue +  include ProjectStartImport    sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION @@ -37,7 +38,7 @@ class RepositoryForkWorker    private    def start_fork(project) -    return true if project.import_start +    return true if start(project)      Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.")      false diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 00a021abbdc..d7c0043d3b6 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -4,6 +4,7 @@ class RepositoryImportWorker    include Sidekiq::Worker    include DedicatedSidekiqQueue    include ExceptionBacktrace +  include ProjectStartImport    sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION @@ -34,7 +35,7 @@ class RepositoryImportWorker    private    def start_import(project) -    return true if project.import_start +    return true if start(project)      Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")      false diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb index 7843179d77c..a396c0f27b2 100644 --- a/app/workers/stuck_merge_jobs_worker.rb +++ b/app/workers/stuck_merge_jobs_worker.rb @@ -23,7 +23,7 @@ class StuckMergeJobsWorker      merge_requests = MergeRequest.where(id: completed_ids)      merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged) -    merge_requests.where(merge_commit_sha: nil).update_all(state: :opened) +    merge_requests.where(merge_commit_sha: nil).update_all(state: :opened, merge_jid: nil)      Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}")    end diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb index 89ae17cef37..150788ca611 100644 --- a/app/workers/update_merge_requests_worker.rb +++ b/app/workers/update_merge_requests_worker.rb @@ -2,6 +2,10 @@ class UpdateMergeRequestsWorker    include Sidekiq::Worker    include DedicatedSidekiqQueue +  def metrics_tags +    @metrics_tags || {} +  end +    def perform(project_id, user_id, oldrev, newrev, ref)      project = Project.find_by(id: project_id)      return unless project @@ -9,6 +13,11 @@ class UpdateMergeRequestsWorker      user = User.find_by(id: user_id)      return unless user +    @metrics_tags = { +      project_id: project_id, +      user_id: user_id +    } +      MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)    end  end diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb index 5aa3bbdaa9d..241ed3901dc 100644 --- a/app/workers/wait_for_cluster_creation_worker.rb +++ b/app/workers/wait_for_cluster_creation_worker.rb @@ -2,25 +2,10 @@ class WaitForClusterCreationWorker    include Sidekiq::Worker    include ClusterQueue -  INITIAL_INTERVAL = 2.minutes -  EAGER_INTERVAL = 10.seconds -  TIMEOUT = 20.minutes -    def perform(cluster_id) -    Gcp::Cluster.find_by_id(cluster_id).try do |cluster| -      Ci::FetchGcpOperationService.new.execute(cluster) do |operation| -        case operation.status -        when 'RUNNING' -          if TIMEOUT < Time.now.utc - operation.start_time.to_time.utc -            return cluster.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}") -          end - -          WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, cluster.id) -        when 'DONE' -          Ci::FinalizeClusterCreationService.new.execute(cluster) -        else -          return cluster.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}") -        end +    Clusters::Cluster.find_by_id(cluster_id).try do |cluster| +      cluster.provider.try do |provider| +        Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) if cluster.gcp?        end      end    end  | 
