diff options
Diffstat (limited to 'app')
367 files changed, 5042 insertions, 2247 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 differnew file mode 100644 index 00000000000..b1327b4f7b4 --- /dev/null +++ b/app/assets/images/auth_buttons/signin_with_google.png diff --git a/app/assets/images/icon_image_comment.svg b/app/assets/images/icon_image_comment.svg new file mode 100644 index 00000000000..cf6cb972940 --- /dev/null +++ b/app/assets/images/icon_image_comment.svg @@ -0,0 +1 @@ +<svg width="24" height="30" viewBox="0 0 24 30" xmlns="http://www.w3.org/2000/svg"><title>cursor</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="#1F78D1" 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="#FFF"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787c-.91 0-1.763.156-2.558.469-.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-.068 0-.009.01-.031.033-.067a.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-.26.094-.126.168-.24.221-.342.054-.103.114-.235.181-.395.067-.161.125-.33.174-.51-.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="#1F78D1" fill-rule="nonzero"/></g></svg> diff --git a/app/assets/images/icon_image_comment@2x.svg b/app/assets/images/icon_image_comment@2x.svg new file mode 100644 index 00000000000..83be91d3705 --- /dev/null +++ b/app/assets/images/icon_image_comment@2x.svg @@ -0,0 +1 @@ +<svg width="48" height="60" viewBox="0 0 48 60" xmlns="http://www.w3.org/2000/svg"><title>cursor_2x</title><g fill="none" fill-rule="evenodd"><path d="M48 24.21C48 37.583 36.522 47.369 24 60 11.478 47.368 0 37.582 0 24.21 0 10.84 10.745 0 24 0s24 10.84 24 24.21z" fill="#1F78D1" fill-rule="nonzero"/><path d="M30.56 50.497c2.915-2.95 5.078-5.268 6.947-7.493 5.703-6.788 8.406-12.53 8.406-18.793 0-12.223-9.815-22.124-21.913-22.124S2.087 11.988 2.087 24.211c0 6.263 2.703 12.005 8.406 18.793 1.87 2.225 4.032 4.544 6.947 7.493 1.022 1.035 4.432 4.426 6.56 6.55 2.128-2.124 5.538-5.515 6.56-6.55z" fill="#FFF"/><path d="M29.103 16.512c-1.58-.625-3.282-.938-5.103-.938-1.821 0-3.527.313-5.116.938-1.58.616-2.84 1.45-3.777 2.504-.928 1.054-1.393 2.192-1.393 3.415 0 1 .317 1.956.951 2.866.643.902 1.545 1.684 2.706 2.344l1.165.67-.362 1.286a9.603 9.603 0 0 1-.937 2.303 13.208 13.208 0 0 0 3.683-2.29l.576-.509.763.08c.616.072 1.196.108 1.741.108 1.821 0 3.522-.308 5.103-.925 1.589-.625 2.848-1.464 3.776-2.517.938-1.054 1.407-2.192 1.407-3.416 0-1.223-.469-2.361-1.407-3.415-.928-1.053-2.187-1.888-3.776-2.504zm5.29 1.62c1.071 1.313 1.607 2.746 1.607 4.3 0 1.553-.536 2.99-1.607 4.312-1.072 1.312-2.527 2.353-4.366 3.12-1.84.76-3.848 1.139-6.027 1.139a18.32 18.32 0 0 1-1.942-.107c-1.768 1.562-3.821 2.643-6.16 3.24-.438.126-.947.224-1.527.295h-.067a.521.521 0 0 1-.362-.147.649.649 0 0 1-.214-.362v-.013c-.027-.036-.032-.09-.014-.16.027-.072.036-.117.027-.135 0-.017.022-.062.067-.133a1.29 1.29 0 0 0 .08-.121c.01-.009.04-.045.094-.107a106.068 106.068 0 0 1 .522-.59c.215-.232.367-.401.456-.508.098-.099.236-.273.415-.523.188-.25.335-.477.442-.683.107-.205.228-.468.362-.79.134-.321.25-.66.348-1.018-1.402-.794-2.51-1.777-3.322-2.946C12.402 25.025 12 23.77 12 22.43c0-1.553.536-2.986 1.607-4.299 1.072-1.321 2.527-2.361 4.366-3.12 1.84-.768 3.848-1.152 6.027-1.152 2.179 0 4.188.384 6.027 1.152 1.84.759 3.294 1.799 4.366 3.12z" fill="#1F78D1" fill-rule="nonzero"/></g></svg> diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json index 6b8f85e37fd..c0ed2ffdcb2 100644 --- a/app/assets/images/icons.json +++ b/app/assets/images/icons.json @@ -1 +1 @@ -{"iconCount":135,"spriteSize":58718,"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","comment-dots","comment-next","comment","comments","commit","credit-card","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","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","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","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","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":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 diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg index 30cb2109ec2..b9829d0d450 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.535 7.95L1.292 3.707a1 1 0 0 1-.281-.695 1 1 0 1 1 1.695-.719l4.95 4.95a.998.998 0 0 1 0 1.414l-4.95 4.95a1.002 1.002 0 0 1-.707.293c-.549 0-1-.452-1-1 0-.266.106-.52.293-.708L5.535 7.95zm7 0L8.292 3.707a1 1 0 0 1-.281-.695 1 1 0 1 1 1.695-.719l4.95 4.95a.998.998 0 0 1 0 1.414l-4.95 4.95a1.002 1.002 0 0 1-.707.293c-.549 0-1-.452-1-1.001 0-.265.106-.519.293-.707l4.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 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="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 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="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 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 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="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 diff --git a/app/assets/images/new_nav.png b/app/assets/images/new_nav.png Binary files differdeleted file mode 100644 index f98ca15d787..00000000000 --- a/app/assets/images/new_nav.png +++ /dev/null diff --git a/app/assets/images/old_nav.png b/app/assets/images/old_nav.png Binary files differdeleted file mode 100644 index 23fae7aa19e..00000000000 --- a/app/assets/images/old_nav.png +++ /dev/null diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js index 346de4ad11e..3de192d56eb 100644 --- a/app/assets/javascripts/abuse_reports.js +++ b/app/assets/javascripts/abuse_reports.js @@ -1,7 +1,7 @@ const MAX_MESSAGE_LENGTH = 500; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; -class AbuseReports { +export default class AbuseReports { constructor() { $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage); $(document) @@ -32,6 +32,3 @@ class AbuseReports { } } } - -window.gl = window.gl || {}; -window.gl.AbuseReports = AbuseReports; diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js index 8f5e2e545ec..2bc77859c26 100644 --- a/app/assets/javascripts/ajax_loading_spinner.js +++ b/app/assets/javascripts/ajax_loading_spinner.js @@ -1,4 +1,4 @@ -class AjaxLoadingSpinner { +export default class AjaxLoadingSpinner { static init() { const $elements = $('.js-ajax-loading-spinner'); @@ -30,6 +30,3 @@ class AjaxLoadingSpinner { classList.toggle('fa-spin'); } } - -window.gl = window.gl || {}; -gl.AjaxLoadingSpinner = AjaxLoadingSpinner; 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/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/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..7f3afefc9cc 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -3,9 +3,9 @@ /* 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'; 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/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index d7f203b3f96..c19c989680d 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -1,6 +1,7 @@ -/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, +/* eslint-disable func-names, no-new, space-before-function-paren, one-var, promise/catch-or-return */ import _ from 'underscore'; +import CreateLabelDropdown from '../../create_label'; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -15,15 +16,15 @@ $(document).off('created.label').on('created.label', (e, label) => { label: { id: label.id, title: label.title, - color: label.color - } + color: label.color, + }, }); }); gl.issueBoards.newListDropdownInit = () => { $('.js-new-board-list').each(function () { const $this = $(this); - new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); + new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); $this.glDropdown({ data(term, callback) { @@ -38,17 +39,17 @@ gl.issueBoards.newListDropdownInit = () => { const $a = $('<a />', { class: (active ? `is-active js-board-list-${active.id}` : ''), text: label.title, - href: '#' + href: '#', }); const $labelColor = $('<span />', { class: 'dropdown-label-box', - style: `background-color: ${label.color}` + style: `background-color: ${label.color}`, }); return $li.append($a.prepend($labelColor)); }, search: { - fields: ['title'] + fields: ['title'], }, filterable: true, selectable: true, @@ -66,13 +67,13 @@ gl.issueBoards.newListDropdownInit = () => { label: { id: label.id, title: label.title, - color: label.color - } + color: label.color, + }, }); Store.state.lists = _.sortBy(Store.state.lists, 'position'); } - } + }, }); }); }; 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/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index bd479700fd3..ace89398943 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -1,25 +1,45 @@ -/* 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) { - return window.location = this.dataset.link; + } + // 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)); + }); + } + // 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 + // We handle all the showing/hiding below + trigger: 'manual', }); - }; - return BuildArtifacts; -})(); + // We want the tooltip to show if you hover anywhere on the row + // But be placed below and in the middle of the file name + $('.js-artifact-tree-row') + .on('mouseenter', (e) => { + $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('show'); + }) + .on('mouseleave', (e) => { + $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide'); + }); + } +} 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 new file mode 100644 index 00000000000..50dbeb06362 --- /dev/null +++ b/app/assets/javascripts/clusters.js @@ -0,0 +1,112 @@ +/* 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/commit.js b/app/assets/javascripts/commit.js deleted file mode 100644 index 5f637524e30..00000000000 --- a/app/assets/javascripts/commit.js +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife */ -/* global CommitFile */ - -window.Commit = (function() { - function Commit() { - $('.files .diff-file').each(function() { - return new CommitFile(this); - }); - } - - return Commit; -})(); diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js deleted file mode 100644 index ee087c978dd..00000000000 --- a/app/assets/javascripts/commit/file.js +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new */ -/* global ImageFile */ - -(function() { - this.CommitFile = (function() { - function CommitFile(file) { - if ($('.image', file).length) { - new gl.ImageFile(file); - } - } - - return CommitFile; - })(); -}).call(window); diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 4763985c802..e7adf8814b8 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */ +import 'vendor/jquery.waitforimages'; + (function() { gl.ImageFile = (function() { var prepareFrames; @@ -17,15 +19,10 @@ // Load two-up view after images are loaded // so that we can display the correct width and height information - const images = $('.two-up.view img', _this.file); - let loadedCount = 0; - - images.on('load', () => { - loadedCount += 1; + const $images = $('.two-up.view img', _this.file); - if (loadedCount === images.length) { - _this.initView('two-up'); - } + $images.waitForImages(function() { + _this.initView('two-up'); }); }); }; diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 047544b1762..ae6b8902032 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,17 +1,19 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */ +/* eslint-disable func-names, wrap-iife, consistent-return, + no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars, + prefer-template, object-shorthand, prefer-arrow-callback */ /* global Pager */ -window.CommitsList = (function() { - var CommitsList = {}; +export default (function () { + const CommitsList = {}; CommitsList.timer = null; - CommitsList.init = function(limit) { + CommitsList.init = function (limit) { this.$contentList = $('.content_list'); - $("body").on("click", ".day-commits-table li.commit", function(e) { - if (e.target.nodeName !== "A") { - location.href = $(this).attr("url"); + $('body').on('click', '.day-commits-table li.commit', function (e) { + if (e.target.nodeName !== 'A') { + location.href = $(this).attr('url'); e.stopPropagation(); return false; } @@ -19,48 +21,47 @@ window.CommitsList = (function() { Pager.init(parseInt(limit, 10), false, false, this.processCommits); - this.content = $("#commits-list"); - this.searchField = $("#commits-search"); + this.content = $('#commits-list'); + this.searchField = $('#commits-search'); this.lastSearch = this.searchField.val(); return this.initSearch(); }; - CommitsList.initSearch = function() { + CommitsList.initSearch = function () { this.timer = null; - return this.searchField.keyup((function(_this) { - return function() { + return this.searchField.keyup((function (_this) { + return function () { clearTimeout(_this.timer); return _this.timer = setTimeout(_this.filterResults, 500); }; })(this)); }; - CommitsList.filterResults = function() { - var commitsUrl, form, search; - form = $(".commits-search-form"); - search = CommitsList.searchField.val(); + CommitsList.filterResults = function () { + const form = $('.commits-search-form'); + const search = CommitsList.searchField.val(); if (search === CommitsList.lastSearch) return; - commitsUrl = form.attr("action") + '?' + form.serialize(); + const commitsUrl = form.attr('action') + '?' + form.serialize(); CommitsList.content.fadeTo('fast', 0.5); return $.ajax({ - type: "GET", - url: form.attr("action"), + type: 'GET', + url: form.attr('action'), data: form.serialize(), - complete: function() { + complete: function () { return CommitsList.content.fadeTo('fast', 1.0); }, - success: function(data) { + success: function (data) { CommitsList.lastSearch = search; CommitsList.content.html(data.html); return history.replaceState({ - page: commitsUrl + page: commitsUrl, // Change url so if user reload a page - search results are saved }, document.title, commitsUrl); }, - error: function() { + error: function () { CommitsList.lastSearch = null; }, - dataType: "json" + dataType: 'json', }); }; @@ -81,7 +82,7 @@ window.CommitsList = (function() { commitsCount = $commitsHeadersLast.nextUntil('li.js-commit-header').find('li.commit').length; // Remove duplicate of commits header. - processedData = $processedData.not(`li.js-commit-header[data-day="${loadedShownDayFirst}"]`); + processedData = $processedData.not(`li.js-commit-header[data-day='${loadedShownDayFirst}']`); // Update commits count in the previous commits header. commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length); diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js index 907b468e576..3bed0678350 100644 --- a/app/assets/javascripts/create_label.js +++ b/app/assets/javascripts/create_label.js @@ -1,8 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */ +/* eslint-disable func-names, prefer-arrow-callback */ import Api from './api'; -class CreateLabelDropdown { - constructor ($el, namespacePath, projectPath) { +export default class CreateLabelDropdown { + constructor($el, namespacePath, projectPath) { this.$el = $el; this.namespacePath = namespacePath; this.projectPath = projectPath; @@ -22,7 +22,7 @@ class CreateLabelDropdown { this.addBinding(); } - cleanBinding () { + cleanBinding() { this.$colorSuggestions.off('click'); this.$newLabelField.off('keyup change'); this.$newColorField.off('keyup change'); @@ -31,7 +31,7 @@ class CreateLabelDropdown { this.$newLabelCreateButton.off('click'); } - addBinding () { + addBinding() { const self = this; this.$colorSuggestions.on('click', function (e) { @@ -44,7 +44,7 @@ class CreateLabelDropdown { this.$dropdownBack.on('click', this.resetForm.bind(this)); - this.$cancelButton.on('click', function(e) { + this.$cancelButton.on('click', function (e) { e.preventDefault(); e.stopPropagation(); @@ -55,7 +55,7 @@ class CreateLabelDropdown { this.$newLabelCreateButton.on('click', this.saveLabel.bind(this)); } - addColorValue (e, $this) { + addColorValue(e, $this) { e.preventDefault(); e.stopPropagation(); @@ -66,7 +66,7 @@ class CreateLabelDropdown { .addClass('is-active'); } - enableLabelCreateButton () { + enableLabelCreateButton() { if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') { this.$newLabelError.hide(); this.$newLabelCreateButton.enable(); @@ -75,7 +75,7 @@ class CreateLabelDropdown { } } - resetForm () { + resetForm() { this.$newLabelField .val('') .trigger('change'); @@ -90,13 +90,13 @@ class CreateLabelDropdown { .removeClass('is-active'); } - saveLabel (e) { + saveLabel(e) { e.preventDefault(); e.stopPropagation(); Api.newLabel(this.namespacePath, this.projectPath, { title: this.$newLabelField.val(), - color: this.$newColorField.val() + color: this.$newColorField.val(), }, (label) => { this.$newLabelCreateButton.enable(); @@ -107,8 +107,8 @@ class CreateLabelDropdown { errors = label.message; } else { errors = Object.keys(label.message).map(key => - `${gl.text.humanize(key)} ${label.message[key].join(', ')}` - ).join("<br/>"); + `${gl.text.humanize(key)} ${label.message[key].join(', ')}`, + ).join('<br/>'); } this.$newLabelError @@ -122,6 +122,3 @@ class CreateLabelDropdown { }); } } - -window.gl = window.gl || {}; -gl.CreateLabelDropdown = CreateLabelDropdown; 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/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue new file mode 100644 index 00000000000..732697c134e --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/banner.vue @@ -0,0 +1,55 @@ +<script> + import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg'; + + export default { + props: { + documentationLink: { + type: String, + required: true, + }, + }, + computed: { + iconCycleAnalyticsSplash() { + return iconCycleAnalyticsSplash; + }, + }, + methods: { + dismissOverviewDialog() { + this.$emit('dismiss-overview-dialog'); + }, + }, + }; +</script> +<template> + <div class="landing content-block"> + <button + class="js-ca-dismiss-button dismiss-button" + type="button" + :aria-label="__('Dismiss Cycle Analytics introduction box')" + @click="dismissOverviewDialog"> + <i + class="fa fa-times" + aria-hidden="true"> + </i> + </button> + <div class="svg-container" v-html="iconCycleAnalyticsSplash"> + </div> + <div class="inner-content"> + <h4> + {{__('Introducing Cycle Analytics')}} + </h4> + <p> + {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }} + </p> + <p> + <a + :href="documentationLink" + target="_blank" + rel="nofollow" + class="btn"> + {{__('Read more')}} + </a> + </p> + </div> + </div> +</template> diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue index 9941b997b3f..62efd4f9c28 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue @@ -20,7 +20,7 @@ <template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template> <template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template> <template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template> - <template v-if="time.seconds && hasDa === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template> + <template v-if="time.seconds && hasData === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template> </template> <template v-else> -- diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 991fcf114da..49bb6c52180 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -1,8 +1,8 @@ -/* 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'; import stagePlanComponent from './components/stage_plan_component.vue'; import stageComponent from './components/stage_component.vue'; @@ -44,6 +44,7 @@ $(() => { }, }, components: { + banner, 'stage-issue-component': stageComponent, 'stage-plan-component': stagePlanComponent, 'stage-code-component': stageCodeComponent, 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 ae8338f5fd2..6c78662baa7 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -3,6 +3,7 @@ import './lib/utils/url_utility'; import FilesCommentButton from './files_comment_button'; import SingleFileDiff from './single_file_diff'; +import imageDiffHelper from './image_diff/helpers/index'; const UNFOLD_COUNT = 20; let isBound = false; @@ -17,9 +18,12 @@ class Diff { } }); - FilesCommentButton.init($diffFile); + const tab = document.getElementById('diffs'); + if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile); - $diffFile.each((index, file) => new gl.ImageFile(file)); + const firstFile = $('.files').first().get(0); + const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note'); + $diffFile.each((index, file) => imageDiffHelper.initImageDiff(file, canCreateNote)); if (!isBound) { $(document) diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js index 497c23f014f..e77910a83d4 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -171,7 +171,14 @@ const JumpToDiscussion = Vue.extend({ // When jumping between unresolved discussions on the diffs tab, we show them. $target.closest(".content").show(); - $target = $target.closest("tr.notes_holder"); + const $notesHolder = $target.closest("tr.notes_holder"); + + // Image diff discussions does not use notes_holder + // so we should keep original $target value in those cases + if ($notesHolder.length > 0) { + $target = $notesHolder; + } + $target.show(); // If we are on the diffs tab, we don't scroll to the discussion itself, but to 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 4f9849d72f6..225cafbb7d4 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -7,14 +7,13 @@ /* global IssuableForm */ /* global LabelsSelect */ /* global MilestoneSelect */ -/* global Commit */ -/* global CommitsList */ /* global NewBranchForm */ /* global NotificationsForm */ /* global NotificationsDropdown */ /* global GroupAvatar */ /* global LineHighlighter */ -/* global BuildArtifacts */ +import BuildArtifacts from './build_artifacts'; +import CILintEditor from './ci_lint_editor'; /* global GroupsSelect */ /* global Search */ /* global Admin */ @@ -36,6 +35,7 @@ /* global Sidebar */ /* global ShortcutsWiki */ +import CommitsList from './commits'; import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; import DeleteModal from './branches/branches_delete_modal'; @@ -77,7 +77,10 @@ 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 U2FAuthenticate from './u2f/authenticate'; (function() { var Dispatcher; @@ -90,8 +93,8 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; } 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; } @@ -239,7 +242,7 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)); break; case 'projects:branches:index': - gl.AjaxLoadingSpinner.init(); + AjaxLoadingSpinner.init(); new DeleteModal(); break; case 'projects:issues:new': @@ -317,7 +320,6 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; new gl.Activities(); break; case 'projects:commit:show': - new Commit(); new gl.Diff(); new ZenMode(); shortcut_handler = new ShortcutsNavigation(); @@ -511,7 +513,7 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; break; case 'ci:lints:create': case 'ci:lints:show': - new gl.CILintEditor(); + new CILintEditor(); break; case 'users:show': new UserCallout(); @@ -531,19 +533,26 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; case 'admin:impersonation_tokens:index': new gl.DueDateSelectors(); break; + case 'projects:clusters:show': + import(/* webpackChunkName: "clusters" */ './clusters') + .then(cluster => new cluster.default()) // eslint-disable-line new-cap + .catch(() => {}); + 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]) { @@ -563,7 +572,7 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; new Labels(); } case 'abuse_reports': - new gl.AbuseReports(); + new AbuseReports(); break; } break; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 1cba65d17cd..bd45da8c422 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -237,9 +237,12 @@ window.DropzoneInput = (function() { }; const insertToTextArea = function(filename, url) { - return $(child).val(function(index, val) { + const $child = $(child); + $child.val(function(index, val) { return val.replace(`{{${filename}}}`, url); }); + + $child.trigger('change'); }; const appendToTextArea = function(url) { 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/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/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/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index a44dc279a6f..7b233842d5a 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 []; }) 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..dd24fc44d2a 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'; diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index ccff8f0ace7..bc5cd818e1c 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -1,71 +1,94 @@ -/* 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 */ +import _ from 'underscore'; -window.Flash = (function() { - var hideFlash; +const hideFlash = (flashEl, fadeTransition = true) => { + if (fadeTransition) { + Object.assign(flashEl.style, { + transition: 'opacity .3s', + opacity: '0', + }); + } - hideFlash = function() { - return $(this).fadeOut(); - }; + flashEl.addEventListener('transitionend', () => { + flashEl.remove(); + }, { + once: true, + passive: true, + }); - /** - * 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 - }); - textDiv.appendTo(flash); + if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend')); +}; - if (actionConfig) { - const actionLinkConfig = { - class: 'flash-action', - href: actionConfig.href || '#', - text: actionConfig.title - }; +const createAction = config => ` + <a + href="${config.href || '#'}" + class="flash-action" + ${config.href ? '' : 'role="button"'} + > + ${_.escape(config.title)} + </a> +`; - if (!actionConfig.href) { - actionLinkConfig.role = 'button'; - } +const createFlashEl = (message, type, isInContentWrapper = false) => ` + <div + class="flash-${type}" + > + <div + class="flash-text ${isInContentWrapper ? 'container-fluid container-limited' : ''}" + > + ${_.escape(message)} + </div> + </div> +`; - actionLink = $('<a/>', actionLinkConfig); +/* + * 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'); - actionLink.appendTo(flash); - this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler); - } - if (this.flashContainer.parent().hasClass('content-wrapper')) { - textDiv.addClass('container-fluid container-limited'); + if (!flashContainer) return null; + + const isInContentWrapper = flashContainer.parentNode.classList.contains('content-wrapper'); + + flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper); + + const flashEl = flashContainer.querySelector(`.flash-${type}`); + flashEl.addEventListener('click', () => hideFlash(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, +}; +window.Flash = createFlash; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index ff218ccad62..e8d8fef8579 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -738,7 +738,7 @@ GitLabDropdown = (function() { : selectedObject.id; if (isInput) { field = $(this.el); - } else if (value) { + } else if (value != null) { field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); } @@ -746,7 +746,7 @@ GitLabDropdown = (function() { return; } - if (el.hasClass(ACTIVE_CLASS)) { + if (el.hasClass(ACTIVE_CLASS) && value !== 0) { isMarking = false; el.removeClass(ACTIVE_CLASS); if (field && field.length) { @@ -852,7 +852,7 @@ GitLabDropdown = (function() { if (href && href !== '#') { gl.utils.visitUrl(href); } else { - $el.first().trigger('click'); + $el.trigger('click'); } } }; diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 84286c3572e..35b96620209 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,8 +1,6 @@ -/* global Flash */ - import Vue from 'vue'; - import Translate from '../vue_shared/translate'; +import Flash from '../flash'; import GroupFilterableList from './groups_filterable_list'; import GroupsStore from './store/groups_store'; import GroupsService from './service/groups_service'; diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index dc170c60456..ea2e2205077 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,7 +1,16 @@ -/* 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 + */ +$(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/image_diff/helpers/badge_helper.js b/app/assets/javascripts/image_diff/helpers/badge_helper.js new file mode 100644 index 00000000000..6a6a668308d --- /dev/null +++ b/app/assets/javascripts/image_diff/helpers/badge_helper.js @@ -0,0 +1,38 @@ +export function createImageBadge(noteId, { x, y }, classNames = []) { + const buttonEl = document.createElement('button'); + const classList = classNames.concat(['js-image-badge']); + classList.forEach(className => buttonEl.classList.add(className)); + buttonEl.setAttribute('type', 'button'); + buttonEl.setAttribute('disabled', true); + buttonEl.dataset.noteId = noteId; + buttonEl.style.left = `${x}px`; + buttonEl.style.top = `${y}px`; + + return buttonEl; +} + +export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) { + const buttonEl = createImageBadge(noteId, coordinate, ['badge']); + buttonEl.innerText = badgeText; + + containerEl.appendChild(buttonEl); +} + +export function addImageCommentBadge(containerEl, { coordinate, noteId }) { + const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge', 'inverted']); + const iconEl = document.createElement('i'); + iconEl.className = 'fa fa-comment-o'; + iconEl.setAttribute('aria-label', 'comment'); + + buttonEl.appendChild(iconEl); + containerEl.appendChild(buttonEl); +} + +export function addAvatarBadge(el, event) { + const { noteId, badgeNumber } = event.detail; + + // Add badge to new comment + const avatarBadgeEl = el.querySelector(`#${noteId} .badge`); + avatarBadgeEl.innerText = badgeNumber; + avatarBadgeEl.classList.remove('hidden'); +} diff --git a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js new file mode 100644 index 00000000000..05000c73052 --- /dev/null +++ b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js @@ -0,0 +1,58 @@ +export function addCommentIndicator(containerEl, { x, y }) { + const buttonEl = document.createElement('button'); + buttonEl.classList.add('btn-transparent'); + buttonEl.classList.add('comment-indicator'); + buttonEl.setAttribute('type', 'button'); + buttonEl.style.left = `${x}px`; + buttonEl.style.top = `${y}px`; + + buttonEl.innerHTML = gl.utils.spriteIcon('image-comment-dark'); + + containerEl.appendChild(buttonEl); +} + +export function removeCommentIndicator(imageFrameEl) { + const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator'); + const imageEl = imageFrameEl.querySelector('img'); + const willRemove = !!commentIndicatorEl; + let meta = {}; + + if (willRemove) { + meta = { + x: parseInt(commentIndicatorEl.style.left, 10), + y: parseInt(commentIndicatorEl.style.top, 10), + image: { + width: imageEl.width, + height: imageEl.height, + }, + }; + + commentIndicatorEl.remove(); + } + + return Object.assign({}, meta, { + removed: willRemove, + }); +} + +export function showCommentIndicator(imageFrameEl, coordinate) { + const { x, y } = coordinate; + const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator'); + + if (commentIndicatorEl) { + commentIndicatorEl.style.left = `${x}px`; + commentIndicatorEl.style.top = `${y}px`; + } else { + addCommentIndicator(imageFrameEl, coordinate); + } +} + +export function commentIndicatorOnClick(event) { + // Prevent from triggering onAddImageDiffNote in notes.js + event.stopPropagation(); + + const buttonEl = event.currentTarget; + const diffViewerEl = buttonEl.closest('.diff-viewer'); + const textareaEl = diffViewerEl.querySelector('.note-container .note-textarea'); + textareaEl.focus(); +} diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js new file mode 100644 index 00000000000..12d56714b34 --- /dev/null +++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js @@ -0,0 +1,44 @@ +export function setPositionDataAttribute(el, options) { + // Update position data attribute so that the + // new comment form can use this data for ajax request + const { x, y, width, height } = options; + const position = el.dataset.position; + const positionObject = Object.assign({}, JSON.parse(position), { + x, + y, + width, + height, + }); + + el.setAttribute('data-position', JSON.stringify(positionObject)); +} + +export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) { + const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .badge'); + avatarBadgeEl.innerText = newBadgeNumber; +} + +export function updateDiscussionBadgeNumber(discussionEl, newBadgeNumber) { + const discussionBadgeEl = discussionEl.querySelector('.badge'); + discussionBadgeEl.innerText = newBadgeNumber; +} + +export function toggleCollapsed(event) { + const toggleButtonEl = event.currentTarget; + const discussionNotesEl = toggleButtonEl.closest('.discussion-notes'); + const formEl = discussionNotesEl.querySelector('.discussion-form'); + const isCollapsed = discussionNotesEl.classList.contains('collapsed'); + + if (isCollapsed) { + discussionNotesEl.classList.remove('collapsed'); + } else { + discussionNotesEl.classList.add('collapsed'); + } + + // Override the inline display style set in notes.js + if (formEl && !isCollapsed) { + formEl.style.display = 'none'; + } else if (formEl && isCollapsed) { + formEl.style.display = 'block'; + } +} diff --git a/app/assets/javascripts/image_diff/helpers/index.js b/app/assets/javascripts/image_diff/helpers/index.js new file mode 100644 index 00000000000..4a100631003 --- /dev/null +++ b/app/assets/javascripts/image_diff/helpers/index.js @@ -0,0 +1,25 @@ +import * as badgeHelper from './badge_helper'; +import * as commentIndicatorHelper from './comment_indicator_helper'; +import * as domHelper from './dom_helper'; +import * as utilsHelper from './utils_helper'; + +export default { + addCommentIndicator: commentIndicatorHelper.addCommentIndicator, + removeCommentIndicator: commentIndicatorHelper.removeCommentIndicator, + showCommentIndicator: commentIndicatorHelper.showCommentIndicator, + commentIndicatorOnClick: commentIndicatorHelper.commentIndicatorOnClick, + + addImageBadge: badgeHelper.addImageBadge, + addImageCommentBadge: badgeHelper.addImageCommentBadge, + addAvatarBadge: badgeHelper.addAvatarBadge, + + setPositionDataAttribute: domHelper.setPositionDataAttribute, + updateDiscussionAvatarBadgeNumber: domHelper.updateDiscussionAvatarBadgeNumber, + updateDiscussionBadgeNumber: domHelper.updateDiscussionBadgeNumber, + toggleCollapsed: domHelper.toggleCollapsed, + + resizeCoordinatesToImageElement: utilsHelper.resizeCoordinatesToImageElement, + generateBadgeFromDiscussionDOM: utilsHelper.generateBadgeFromDiscussionDOM, + getTargetSelection: utilsHelper.getTargetSelection, + initImageDiff: utilsHelper.initImageDiff, +}; diff --git a/app/assets/javascripts/image_diff/helpers/utils_helper.js b/app/assets/javascripts/image_diff/helpers/utils_helper.js new file mode 100644 index 00000000000..96fc735e629 --- /dev/null +++ b/app/assets/javascripts/image_diff/helpers/utils_helper.js @@ -0,0 +1,95 @@ +import ImageBadge from '../image_badge'; +import ImageDiff from '../image_diff'; +import ReplacedImageDiff from '../replaced_image_diff'; +import '../../commit/image_file'; + +export function resizeCoordinatesToImageElement(imageEl, meta) { + const { x, y, width, height } = meta; + + const imageWidth = imageEl.width; + const imageHeight = imageEl.height; + + const widthRatio = imageWidth / width; + const heightRatio = imageHeight / height; + + return { + x: Math.round(x * widthRatio), + y: Math.round(y * heightRatio), + width: imageWidth, + height: imageHeight, + }; +} + +export function generateBadgeFromDiscussionDOM(imageFrameEl, discussionEl) { + const position = JSON.parse(discussionEl.dataset.position); + const firstNoteEl = discussionEl.querySelector('.note'); + const badge = new ImageBadge({ + actual: position, + imageEl: imageFrameEl.querySelector('img'), + noteId: firstNoteEl.id, + discussionId: discussionEl.dataset.discussionId, + }); + + return badge; +} + +export function getTargetSelection(event) { + const containerEl = event.currentTarget; + const imageEl = containerEl.querySelector('img'); + + const x = event.offsetX; + const y = event.offsetY; + + const width = imageEl.width; + const height = imageEl.height; + + const actualWidth = imageEl.naturalWidth; + const actualHeight = imageEl.naturalHeight; + + const widthRatio = actualWidth / width; + const heightRatio = actualHeight / height; + + // Browser will include the frame as a clickable target, + // which would result in potential 1px out of bounds value + // This bound the coordinates to inside the frame + const normalizedX = Math.max(0, x) && Math.min(x, width); + const normalizedY = Math.max(0, y) && Math.min(y, height); + + return { + browser: { + x: normalizedX, + y: normalizedY, + width, + height, + }, + actual: { + // Round x, y so that we don't need to deal with decimals + x: Math.round(normalizedX * widthRatio), + y: Math.round(normalizedY * heightRatio), + width: actualWidth, + height: actualHeight, + }, + }; +} + +export function initImageDiff(fileEl, canCreateNote, renderCommentBadge) { + const options = { + canCreateNote, + renderCommentBadge, + }; + let diff; + + // ImageFile needs to be invoked before initImageDiff so that badges + // can mount to the correct location + new gl.ImageFile(fileEl); // eslint-disable-line no-new + + if (fileEl.querySelector('.diff-file .js-single-image')) { + diff = new ImageDiff(fileEl, options); + diff.init(); + } else if (fileEl.querySelector('.diff-file .js-replaced-image')) { + diff = new ReplacedImageDiff(fileEl, options); + diff.init(); + } + + return diff; +} diff --git a/app/assets/javascripts/image_diff/image_badge.js b/app/assets/javascripts/image_diff/image_badge.js new file mode 100644 index 00000000000..51a8cda98d7 --- /dev/null +++ b/app/assets/javascripts/image_diff/image_badge.js @@ -0,0 +1,23 @@ +import imageDiffHelper from './helpers/index'; + +const defaultMeta = { + x: 0, + y: 0, + width: 0, + height: 0, +}; + +export default class ImageBadge { + constructor(options) { + const { noteId, discussionId } = options; + + this.actual = options.actual || defaultMeta; + this.browser = options.browser || defaultMeta; + this.noteId = noteId; + this.discussionId = discussionId; + + if (options.imageEl && !options.browser) { + this.browser = imageDiffHelper.resizeCoordinatesToImageElement(options.imageEl, this.actual); + } + } +} diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js new file mode 100644 index 00000000000..f3af92cf2b0 --- /dev/null +++ b/app/assets/javascripts/image_diff/image_diff.js @@ -0,0 +1,143 @@ +import imageDiffHelper from './helpers/index'; +import ImageBadge from './image_badge'; +import { isImageLoaded } from '../lib/utils/image_utility'; + +export default class ImageDiff { + constructor(el, options) { + this.el = el; + this.canCreateNote = !!(options && options.canCreateNote); + this.renderCommentBadge = !!(options && options.renderCommentBadge); + this.$noteContainer = $('.note-container', this.el); + this.imageBadges = []; + } + + init() { + this.imageFrameEl = this.el.querySelector('.diff-file .js-image-frame'); + this.imageEl = this.imageFrameEl.querySelector('img'); + + this.bindEvents(); + } + + bindEvents() { + this.imageClickedWrapper = this.imageClicked.bind(this); + this.imageBlurredWrapper = imageDiffHelper.removeCommentIndicator.bind(null, this.imageFrameEl); + this.addBadgeWrapper = this.addBadge.bind(this); + this.removeBadgeWrapper = this.removeBadge.bind(this); + this.renderBadgesWrapper = this.renderBadges.bind(this); + + // Render badges + if (isImageLoaded(this.imageEl)) { + this.renderBadges(); + } else { + this.imageEl.addEventListener('load', this.renderBadgesWrapper); + } + + // jquery makes the event delegation here much simpler + this.$noteContainer.on('click', '.js-diff-notes-toggle', imageDiffHelper.toggleCollapsed); + $(this.el).on('click', '.comment-indicator', imageDiffHelper.commentIndicatorOnClick); + + if (this.canCreateNote) { + this.el.addEventListener('click.imageDiff', this.imageClickedWrapper); + this.el.addEventListener('blur.imageDiff', this.imageBlurredWrapper); + this.el.addEventListener('addBadge.imageDiff', this.addBadgeWrapper); + this.el.addEventListener('removeBadge.imageDiff', this.removeBadgeWrapper); + } + } + + imageClicked(event) { + const customEvent = event.detail; + const selection = imageDiffHelper.getTargetSelection(customEvent); + const el = customEvent.currentTarget; + + imageDiffHelper.setPositionDataAttribute(el, selection.actual); + imageDiffHelper.showCommentIndicator(this.imageFrameEl, selection.browser); + } + + renderBadges() { + const discussionsEls = this.el.querySelectorAll('.note-container .discussion-notes .notes'); + [...discussionsEls].forEach(this.renderBadge.bind(this)); + } + + renderBadge(discussionEl, index) { + const imageBadge = imageDiffHelper + .generateBadgeFromDiscussionDOM(this.imageFrameEl, discussionEl); + + this.imageBadges.push(imageBadge); + + const options = { + coordinate: imageBadge.browser, + noteId: imageBadge.noteId, + }; + + if (this.renderCommentBadge) { + imageDiffHelper.addImageCommentBadge(this.imageFrameEl, options); + } else { + const numberBadgeOptions = Object.assign({}, options, { + badgeText: index + 1, + }); + + imageDiffHelper.addImageBadge(this.imageFrameEl, numberBadgeOptions); + } + } + + addBadge(event) { + const { x, y, width, height, noteId, discussionId } = event.detail; + const badgeText = this.imageBadges.length + 1; + const imageBadge = new ImageBadge({ + actual: { + x, + y, + width, + height, + }, + imageEl: this.imageFrameEl.querySelector('img'), + noteId, + discussionId, + }); + + this.imageBadges.push(imageBadge); + + imageDiffHelper.addImageBadge(this.imageFrameEl, { + coordinate: imageBadge.browser, + badgeText, + noteId, + }); + + imageDiffHelper.addAvatarBadge(this.el, { + detail: { + noteId, + badgeNumber: badgeText, + }, + }); + + const discussionEl = this.el.querySelector(`#discussion_${discussionId}`); + imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, badgeText); + } + + removeBadge(event) { + const { badgeNumber } = event.detail; + const indexToRemove = badgeNumber - 1; + const imageBadgeEls = this.imageFrameEl.querySelectorAll('.badge'); + + if (this.imageBadges.length !== badgeNumber) { + // Cascade badges count numbers for (avatar badges + image badges) + this.imageBadges.forEach((badge, index) => { + if (index > indexToRemove) { + const { discussionId } = badge; + const updatedBadgeNumber = index; + const discussionEl = this.el.querySelector(`#discussion_${discussionId}`); + + imageBadgeEls[index].innerText = updatedBadgeNumber; + + imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, updatedBadgeNumber); + imageDiffHelper.updateDiscussionAvatarBadgeNumber(discussionEl, updatedBadgeNumber); + } + }); + } + + this.imageBadges.splice(indexToRemove, 1); + + const imageBadgeEl = imageBadgeEls[indexToRemove]; + imageBadgeEl.remove(); + } +} diff --git a/app/assets/javascripts/image_diff/init_discussion_tab.js b/app/assets/javascripts/image_diff/init_discussion_tab.js new file mode 100644 index 00000000000..2f16c6ef115 --- /dev/null +++ b/app/assets/javascripts/image_diff/init_discussion_tab.js @@ -0,0 +1,12 @@ +import imageDiffHelper from './helpers/index'; + +export default () => { + // Always pass can-create-note as false because a user + // cannot place new badge markers on discussion tab + const canCreateNote = false; + const renderCommentBadge = true; + + const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file'); + [...diffFileEls].forEach(diffFileEl => + imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge)); +}; diff --git a/app/assets/javascripts/image_diff/replaced_image_diff.js b/app/assets/javascripts/image_diff/replaced_image_diff.js new file mode 100644 index 00000000000..4abd13fb472 --- /dev/null +++ b/app/assets/javascripts/image_diff/replaced_image_diff.js @@ -0,0 +1,92 @@ +import imageDiffHelper from './helpers/index'; +import { viewTypes, isValidViewType } from './view_types'; +import ImageDiff from './image_diff'; + +export default class ReplacedImageDiff extends ImageDiff { + init(defaultViewType = viewTypes.TWO_UP) { + this.imageFrameEls = { + [viewTypes.TWO_UP]: this.el.querySelector('.two-up .js-image-frame'), + [viewTypes.SWIPE]: this.el.querySelector('.swipe .js-image-frame'), + [viewTypes.ONION_SKIN]: this.el.querySelector('.onion-skin .js-image-frame'), + }; + + const viewModesEl = this.el.querySelector('.view-modes-menu'); + this.viewModesEls = { + [viewTypes.TWO_UP]: viewModesEl.querySelector('.two-up'), + [viewTypes.SWIPE]: viewModesEl.querySelector('.swipe'), + [viewTypes.ONION_SKIN]: viewModesEl.querySelector('.onion-skin'), + }; + + this.currentView = defaultViewType; + this.generateImageEls(); + this.bindEvents(); + } + + generateImageEls() { + this.imageEls = {}; + + const viewTypeNames = Object.getOwnPropertyNames(viewTypes); + viewTypeNames.forEach((viewType) => { + this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img'); + }); + } + + bindEvents() { + super.bindEvents(); + + this.changeToViewTwoUp = this.changeView.bind(this, viewTypes.TWO_UP); + this.changeToViewSwipe = this.changeView.bind(this, viewTypes.SWIPE); + this.changeToViewOnionSkin = this.changeView.bind(this, viewTypes.ONION_SKIN); + + this.viewModesEls[viewTypes.TWO_UP].addEventListener('click', this.changeToViewTwoUp); + this.viewModesEls[viewTypes.SWIPE].addEventListener('click', this.changeToViewSwipe); + this.viewModesEls[viewTypes.ONION_SKIN].addEventListener('click', this.changeToViewOnionSkin); + } + + get imageEl() { + return this.imageEls[this.currentView]; + } + + get imageFrameEl() { + return this.imageFrameEls[this.currentView]; + } + + changeView(newView) { + if (!isValidViewType(newView)) { + return; + } + + const indicator = imageDiffHelper.removeCommentIndicator(this.imageFrameEl); + + this.currentView = newView; + + // Clear existing badges on new view + const existingBadges = this.imageFrameEl.querySelectorAll('.badge'); + [...existingBadges].map(badge => badge.remove()); + + // Remove existing references to old view image badges + this.imageBadges = []; + + // Image_file.js has a fade animation of 200ms for loading the view + // Need to wait an additional 250ms for the images to be displayed + // on window in order to re-normalize their dimensions + setTimeout(this.renderNewView.bind(this, indicator), 250); + } + + renderNewView(indicator) { + // Generate badge coordinates on new view + this.renderBadges(); + + // Re-render indicator in new view + if (indicator.removed) { + const normalizedIndicator = imageDiffHelper + .resizeCoordinatesToImageElement(this.imageEl, { + x: indicator.x, + y: indicator.y, + width: indicator.image.width, + height: indicator.image.height, + }); + imageDiffHelper.showCommentIndicator(this.imageFrameEl, normalizedIndicator); + } + } +} diff --git a/app/assets/javascripts/image_diff/view_types.js b/app/assets/javascripts/image_diff/view_types.js new file mode 100644 index 00000000000..ab0a595571f --- /dev/null +++ b/app/assets/javascripts/image_diff/view_types.js @@ -0,0 +1,9 @@ +export const viewTypes = { + TWO_UP: 'TWO_UP', + SWIPE: 'SWIPE', + ONION_SKIN: 'ONION_SKIN', +}; + +export function isValidViewType(validate) { + return !!Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate); +} 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..eb15949603f 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -1,7 +1,7 @@ /* 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/issue.js b/app/assets/javascripts/issue.js index c0bd64814ca..3fc29f9a661 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,9 +1,7 @@ /* 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'; diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 06f6ec241f4..eecb56cb185 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'; @@ -153,7 +152,7 @@ export default { }) .catch(() => { eventHub.$emit('close.form'); - return new Flash('Error updating issue'); + window.Flash('Error updating issue'); }); }, deleteIssuable() { @@ -167,7 +166,7 @@ export default { }) .catch(() => { eventHub.$emit('close.form'); - return new Flash('Error deleting issue'); + window.Flash('Error deleting issue'); }); }, }, 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/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/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..a8f613c6cf2 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -1,7 +1,8 @@ /* 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 */ +import Flash from './flash'; + ((global) => { class LabelManager { constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 2538d9c2093..d479f7ed682 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,6 +4,7 @@ import _ from 'underscore'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import DropdownUtils from './filtered_search/dropdown_utils'; +import CreateLabelDropdown from './create_label'; (function() { this.LabelsSelect = (function() { @@ -61,7 +62,7 @@ import DropdownUtils from './filtered_search/dropdown_utils'; $sidebarLabelTooltip.tooltip(); if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { - new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath); + new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath); } saveLabelData = function() { diff --git a/app/assets/javascripts/lib/utils/csrf.js b/app/assets/javascripts/lib/utils/csrf.js index ae41cc5e8a8..0bdb547d31a 100644 --- a/app/assets/javascripts/lib/utils/csrf.js +++ b/app/assets/javascripts/lib/utils/csrf.js @@ -14,6 +14,9 @@ If you need to compose a headers object, use the spread operator: someOtherHeader: '12345', } ``` + +see also http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf +and https://github.com/rails/jquery-rails/blob/v4.3.1/vendor/assets/javascripts/jquery_ujs.js#L59-L62 */ const csrf = { @@ -53,4 +56,3 @@ if ($.rails) { } export default csrf; - diff --git a/app/assets/javascripts/lib/utils/image_utility.js b/app/assets/javascripts/lib/utils/image_utility.js new file mode 100644 index 00000000000..2977ec821cb --- /dev/null +++ b/app/assets/javascripts/lib/utils/image_utility.js @@ -0,0 +1,5 @@ +/* eslint-disable import/prefer-default-export */ + +export function isImageLoaded(element) { + return element.complete && element.naturalHeight !== 0; +} 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 3328ff9cc23..78c7a094127 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -1,4 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */ + var base; var w = window; if (w.gl == null) { @@ -86,6 +87,21 @@ w.gl.utils.getLocationHash = function(url) { w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); -w.gl.utils.visitUrl = (url) => { - document.location.href = url; +// eslint-disable-next-line import/prefer-default-export +export function visitUrl(url, external = false) { + if (external) { + // Simulate `target="blank" rel="noopener noreferrer"` + // See https://mathiasbynens.github.io/rel-noopener/ + const otherWindow = window.open(); + otherWindow.opener = null; + otherWindow.location = url; + } else { + document.location.href = url; + } +} + +window.gl = window.gl || {}; +window.gl.utils = { + ...(window.gl.utils || {}), + visitUrl, }; diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index a16d00b5cef..a75d1a4b8d0 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -54,12 +54,14 @@ LineHighlighter.prototype.bindEvents = function() { $fileHolder.on('highlight:line', this.highlightHash); }; -LineHighlighter.prototype.highlightHash = function() { - var range; +LineHighlighter.prototype.highlightHash = function(newHash) { + let range; + if (newHash && typeof newHash === 'string') this._hash = newHash; + + this.clearHighlight(); if (this._hash !== '') { range = this.hashToRange(this._hash); - if (range[0]) { this.highlightRange(range); const lineSelector = `#L${range[0]}`; diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index ce05b3eabec..1003b9ba0af 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -4,6 +4,7 @@ import sprintf from './sprintf'; const langAttribute = document.querySelector('html').getAttribute('lang'); const lang = (langAttribute || 'en').replace(/-/g, '_'); const locale = new Jed(window.translations || {}); +delete window.translations; /** Translates `text` diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 24abc5c5c9e..50aa445e9e7 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 */ @@ -35,12 +34,9 @@ import './shortcuts_network'; import './templates/issuable_template_selector'; import './templates/issuable_template_selectors'; -// commit -import './commit/file'; import './commit/image_file'; // lib/utils -import './lib/utils/bootstrap_linked_tabs'; import { handleLocationHash } from './lib/utils/common_utils'; import './lib/utils/datetime_utility'; import './lib/utils/pretty_time'; @@ -50,40 +46,25 @@ import './lib/utils/url_utility'; // behaviors import './behaviors/'; -// u2f -import './u2f/authenticate'; -import './u2f/error'; -import './u2f/register'; -import './u2f/util'; - // everything else -import './abuse_reports'; import './activities'; import './admin'; -import './ajax_loading_spinner'; -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 './commit'; import './commits'; import './compare'; import './compare_autocomplete'; import './confirm_danger_modal'; import './copy_as_gfm'; import './copy_to_clipboard'; -import './create_label'; import './diff'; import './dropzone_input'; import './due_date_select'; import './files_comment_button'; -import './flash'; +import Flash from './flash'; import './gl_dropdown'; import './gl_field_error'; import './gl_field_errors'; @@ -111,7 +92,6 @@ import './merge_request'; import './merge_request_tabs'; import './milestone'; import './milestone_select'; -import './mini_pipeline_graph_dropdown'; import './namespace_select'; import './new_branch_form'; import './new_commit_form'; @@ -119,7 +99,6 @@ import './notes'; import './notifications_dropdown'; import './notifications_form'; import './pager'; -import './pipelines'; import './preview_markdown'; import './project'; import './project_avatar'; @@ -178,7 +157,6 @@ $(function () { var $document = $(document); var $window = $(window); var $sidebarGutterToggle = $('.js-sidebar-toggle'); - var $flash = $('.flash-container'); var bootstrapBreakpoint = bp.getBreakpointSize(); var fitSidebarForSize; @@ -263,13 +241,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; 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 d3299c15720..df042c7baff 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'; @@ -13,6 +12,8 @@ import { isMetaClick, } from './lib/utils/common_utils'; +import initDiscussionTab from './image_diff/init_discussion_tab'; + /* eslint-disable max-len */ // MergeRequestTabs // @@ -154,6 +155,8 @@ import { } this.resetViewContainer(); this.destroyPipelinesView(); + + initDiscussionTab(); } if (this.setUrl) { this.setCurrentAction(action); 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/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 f80a26b3fd4..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'; @@ -29,6 +29,7 @@ showEmptyState: true, updateAspectRatio: false, updatedAspectRatios: 0, + hoverData: {}, resizeThrottled: {}, }; }, @@ -64,6 +65,10 @@ this.updatedAspectRatios = 0; } }, + + hoverChanged(data) { + this.hoverData = data; + }, }, created() { @@ -72,10 +77,12 @@ deploymentEndpoint: this.deploymentEndpoint, }); eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); + eventHub.$on('hoverChanged', this.hoverChanged); }, beforeDestroy() { eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); + eventHub.$off('hoverChanged', this.hoverChanged); window.removeEventListener('resize', this.resizeThrottled, false); }, @@ -102,6 +109,7 @@ v-for="(graphData, index) in groupData.metrics" :key="index" :graph-data="graphData" + :hover-data="hoverData" :update-aspect-ratio="updateAspectRatio" :deployment-data="store.deploymentData" /> diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index a7b483f6786..a18164482a2 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -73,34 +73,22 @@ <template> <div class="prometheus-state"> - <div class="row"> - <div class="col-md-4 col-md-offset-4 state-svg svg-content"> - <img :src="currentState.svgUrl"/> - </div> + <div class="state-svg svg-content"> + <img :src="currentState.svgUrl"/> </div> - <div class="row"> - <div class="col-md-6 col-md-offset-3"> - <h4 class="text-center state-title"> - {{currentState.title}} - </h4> - </div> - </div> - <div class="row"> - <div class="col-md-6 col-md-offset-3"> - <div class="description-text text-center state-description"> - {{currentState.description}} - <a v-if="showButtonDescription" :href="settingsPath"> - Prometheus server - </a> - </div> - </div> - </div> - <div class="row state-button-section"> - <div class="col-md-4 col-md-offset-4 text-center state-button"> - <a class="btn btn-success" :href="buttonPath"> - {{currentState.buttonText}} - </a> - </div> + <h4 class="state-title"> + {{currentState.title}} + </h4> + <p class="state-description"> + {{currentState.description}} + <a v-if="showButtonDescription" :href="settingsPath"> + Prometheus server + </a> + </p> + <div class="state-button"> + <a class="btn btn-success" :href="buttonPath"> + {{currentState.buttonText}} + </a> </div> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 6b3e341f936..5aa3865f96a 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -3,16 +3,14 @@ import GraphLegend from './graph/legend.vue'; import GraphFlag from './graph/flag.vue'; import GraphDeployment from './graph/deployment.vue'; - import GraphPath from './graph_path.vue'; + import GraphPath from './graph/path.vue'; import MonitoringMixin from '../mixins/monitoring_mixins'; import eventHub from '../event_hub'; import measurements from '../utils/measurements'; - import { timeScaleFormat } from '../utils/date_time_formatters'; + import { timeScaleFormat, bisectDate } from '../utils/date_time_formatters'; import createTimeSeries from '../utils/multiple_time_series'; import bp from '../../breakpoints'; - const bisectDate = d3.bisector(d => d.time).left; - export default { props: { graphData: { @@ -27,6 +25,11 @@ type: Array, required: true, }, + hoverData: { + type: Object, + required: false, + default: () => ({}), + }, }, mixins: [MonitoringMixin], @@ -52,6 +55,7 @@ currentXCoordinate: 0, currentFlagPosition: 0, showFlag: false, + showFlagContent: false, showDeployInfo: true, timeSeries: [], }; @@ -65,7 +69,7 @@ }, computed: { - outterViewBox() { + outerViewBox() { return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`; }, @@ -122,36 +126,30 @@ const d1 = firstTimeSeries.values[overlayIndex]; if (d0 === undefined || d1 === undefined) return; const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; - this.currentData = evalTime ? d1 : d0; - this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1); - this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(this.currentData.time)); + const hoveredDataIndex = evalTime ? overlayIndex : (overlayIndex - 1); + const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time; const currentDeployXPos = this.mouseOverDeployInfo(point.x); - if (this.currentXCoordinate > (this.graphWidth - 200)) { - this.currentFlagPosition = this.currentXCoordinate - 103; - } else { - this.currentFlagPosition = this.currentXCoordinate; - } - - if (currentDeployXPos) { - this.showFlag = false; - } else { - this.showFlag = true; - } + eventHub.$emit('hoverChanged', { + hoveredDate, + currentDeployXPos, + }); }, renderAxesPaths() { - this.timeSeries = createTimeSeries(this.graphData.queries[0], - this.graphWidth, - this.graphHeight, - this.graphHeightOffset); + this.timeSeries = createTimeSeries( + this.graphData.queries[0], + this.graphWidth, + this.graphHeight, + this.graphHeightOffset, + ); if (this.timeSeries.length > 3) { this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20; } const axisXScale = d3.time.scale() - .range([0, this.graphWidth]); + .range([0, this.graphWidth - 70]); const axisYScale = d3.scale.linear() .range([this.graphHeight - this.graphHeightOffset, 0]); @@ -194,6 +192,10 @@ eventHub.$emit('toggleAspectRatio'); } }, + + hoverData() { + this.positionFlag(); + }, }, mounted() { @@ -203,7 +205,10 @@ </script> <template> - <div class="prometheus-graph"> + <div + class="prometheus-graph" + @mouseover="showFlagContent = true" + @mouseleave="showFlagContent = false"> <h5 class="text-center graph-title"> {{graphData.title}} </h5> @@ -211,7 +216,7 @@ class="prometheus-svg-container" :style="paddingBottomRootSvg"> <svg - :viewBox="outterViewBox" + :viewBox="outerViewBox" ref="baseSvg"> <g class="x-axis" @@ -247,6 +252,7 @@ <graph-deployment :show-deploy-info="showDeployInfo" :deployment-data="reducedDeploymentData" + :graph-width="graphWidth" :graph-height="graphHeight" :graph-height-offset="graphHeightOffset" /> @@ -257,6 +263,7 @@ :current-flag-position="currentFlagPosition" :graph-height="graphHeight" :graph-height-offset="graphHeightOffset" + :show-flag-content="showFlagContent" /> <rect class="prometheus-graph-overlay" diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue index 3623d2ed946..e3b8be0c7fb 100644 --- a/app/assets/javascripts/monitoring/components/graph/deployment.vue +++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue @@ -19,6 +19,10 @@ type: Number, required: true, }, + graphWidth: { + type: Number, + required: true, + }, }, computed: { @@ -47,6 +51,14 @@ transformDeploymentGroup(deployment) { return `translate(${Math.floor(deployment.xPos) + 1}, 20)`; }, + + positionFlag(deployment) { + let xPosition = 3; + if (deployment.xPos > (this.graphWidth - 200)) { + xPosition = -97; + } + return xPosition; + }, }, }; </script> @@ -77,7 +89,7 @@ <svg v-if="deployment.showDeploymentFlag" class="js-deploy-info-box" - x="3" + :x="positionFlag(deployment)" y="0" width="92" height="60"> diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index a98e3d06c18..10fb7ff6803 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -23,6 +23,10 @@ type: Number, required: true, }, + showFlagContent: { + type: Boolean, + required: true, + }, }, data() { @@ -57,6 +61,7 @@ transform="translate(-5, 20)"> </line> <svg + v-if="showFlagContent" class="rect-text-metric" :x="currentFlagPosition" y="0"> diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index dbc48c63747..85b6d7f4cbe 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -79,7 +79,11 @@ }, formatMetricUsage(series) { - return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`; + const value = series.values[this.currentDataIndex].value; + if (isNaN(value)) { + return '-'; + } + return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`; }, createSeriesString(index, series) { diff --git a/app/assets/javascripts/monitoring/components/graph_path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue index 043f1bf66bb..043f1bf66bb 100644 --- a/app/assets/javascripts/monitoring/components/graph_path.vue +++ b/app/assets/javascripts/monitoring/components/graph/path.vue diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js index 345a0b37a76..31f38aca5d6 100644 --- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js +++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js @@ -1,3 +1,5 @@ +import { bisectDate } from '../utils/date_time_formatters'; + const mixins = { methods: { mouseOverDeployInfo(mouseXPos) { @@ -18,6 +20,7 @@ const mixins = { return dataFound; }, + formatDeployments() { this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { const time = new Date(deployment.created_at); @@ -40,6 +43,25 @@ const mixins = { return deploymentDataArray; }, []); }, + + positionFlag() { + const timeSeries = this.timeSeries[0]; + const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1); + this.currentData = timeSeries.values[hoveredDataIndex]; + this.currentDataIndex = hoveredDataIndex; + this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time)); + if (this.currentXCoordinate > (this.graphWidth - 200)) { + this.currentFlagPosition = this.currentXCoordinate - 103; + } else { + this.currentFlagPosition = this.currentXCoordinate; + } + + if (this.hoverData.currentDeployXPos) { + this.showFlag = false; + } else { + this.showFlag = true; + } + }, }, }; diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index ef280e02092..104432ef5de 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -3,8 +3,5 @@ import Dashboard from './components/dashboard.vue'; document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#prometheus-graphs', - components: { - Dashboard, - }, - render: createElement => createElement('dashboard'), + render: createElement => createElement(Dashboard), })); diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js index 7592af5878e..854636e9a89 100644 --- a/app/assets/javascripts/monitoring/stores/monitoring_store.js +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -13,7 +13,7 @@ function normalizeMetrics(metrics) { ...result, values: result.values.map(([timestamp, value]) => ({ time: new Date(timestamp * 1000), - value, + value: Number(value), })), })), })), diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js index 26bcaa02511..c4c6b1ac1f5 100644 --- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js +++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js @@ -2,6 +2,7 @@ import d3 from 'd3'; export const dateFormat = d3.time.format('%b %-d, %Y'); export const timeFormat = d3.time.format('%-I:%M%p'); +export const bisectDate = d3.bisector(d => d.time).left; export const timeScaleFormat = d3.time.format.multi([ ['.%L', d => d.getMilliseconds()], diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js index 3cbe06d8fd6..65eec0d8d02 100644 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -56,12 +56,16 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra timeSeriesScaleX.ticks(d3.time.minute, 60); timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]); + const defined = d => !isNaN(d.value) && d.value != null; + const lineFunction = d3.svg.line() + .defined(defined) .interpolate('linear') .x(d => timeSeriesScaleX(d.time)) .y(d => timeSeriesScaleY(d.value)); const areaFunction = d3.svg.area() + .defined(defined) .interpolate('linear') .x(d => timeSeriesScaleX(d.time)) .y0(graphHeight - graphHeightOffset) diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 93aa29454a0..cf7322ba1da 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -5,7 +5,6 @@ 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 */ @@ -18,12 +17,14 @@ import Dropzone from 'dropzone'; 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 loadAwardsHandler from './awards_handler'; import './autosave'; import './dropzone_input'; 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; @@ -42,6 +43,7 @@ export default class Notes { this.visibilityChange = this.visibilityChange.bind(this); this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this); this.onAddDiffNote = this.onAddDiffNote.bind(this); + this.onAddImageDiffNote = this.onAddImageDiffNote.bind(this); this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this); this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this); this.removeNote = this.removeNote.bind(this); @@ -114,6 +116,8 @@ export default class Notes { $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); // add diff note $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote); + // add diff note for images + $(document).on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote); // hide diff note form $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm); // toggle commit list @@ -140,6 +144,7 @@ export default class Notes { $(document).off('click', '.js-note-attachment-delete'); $(document).off('click', '.js-discussion-reply-button'); $(document).off('click', '.js-add-diff-note-button'); + $(document).off('click', '.js-add-image-diff-note-button'); $(document).off('visibilitychange'); $(document).off('keyup input', '.js-note-text'); $(document).off('click', '.js-note-target-reopen'); @@ -349,7 +354,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; @@ -412,6 +417,11 @@ export default class Notes { this.note_ids.push(noteEntity.id); form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); row = form.closest('tr'); + + if (noteEntity.on_image) { + row = form; + } + lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); // is this the first note of discussion? @@ -423,7 +433,7 @@ export default class Notes { if (noteEntity.diff_discussion_html) { var $discussion = $(noteEntity.diff_discussion_html).renderGFM(); - if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) { + if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) { // insert the note and the reply button after the temp row row.after($discussion); } else { @@ -449,6 +459,7 @@ export default class Notes { if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) { gl.diffNotesCompileComponents(); + this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); } @@ -561,7 +572,7 @@ export default class Notes { form.find('#note_line_code').val(), // DiffNote - form.find('#note_position').val() + form.find('#note_position').val(), ]; return new Autosave(textarea, key); } @@ -582,7 +593,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) { @@ -783,9 +794,22 @@ export default class Notes { $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue'); // The notes tr can contain multiple lists of notes, like on the parallel diff - if (notesTr.find('.discussion-notes').length > 1) { + // notesTr does not exist for image diffs + if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) { + const $diffFile = $notes.closest('.diff-file'); + if ($diffFile.length > 0) { + const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', { + detail: { + // badgeNumber's start with 1 and index starts with 0 + badgeNumber: $notes.index() + 1, + }, + }); + + $diffFile[0].dispatchEvent(removeBadgeEvent); + } + $notes.remove(); - } else { + } else if (notesTr.length > 0) { notesTr.remove(); } } @@ -841,7 +865,11 @@ export default class Notes { */ setupDiscussionNoteForm(dataHolder, form) { // setup note target - const diffFileData = dataHolder.closest('.text-file'); + let diffFileData = dataHolder.closest('.text-file'); + + if (diffFileData.length === 0) { + diffFileData = dataHolder.closest('.image'); + } var discussionID = dataHolder.data('discussionId'); @@ -907,6 +935,31 @@ export default class Notes { }); } + onAddImageDiffNote(e) { + const $link = $(e.currentTarget || e.target); + const $diffFile = $link.closest('.diff-file'); + + const clickEvent = new CustomEvent('click.imageDiff', { + detail: e, + }); + + $diffFile[0].dispatchEvent(clickEvent); + + // Setup comment form + let newForm; + const $noteContainer = $link.closest('.diff-viewer').find('.note-container'); + const $form = $noteContainer.find('> .discussion-form'); + + if ($form.length === 0) { + newForm = this.cleanForm(this.formClone.clone()); + newForm.appendTo($noteContainer); + } else { + newForm = $form; + } + + this.setupDiscussionNoteForm($link, newForm); + } + toggleDiffNote({ target, lineType, @@ -999,10 +1052,25 @@ export default class Notes { } cancelDiscussionForm(e) { - var form; e.preventDefault(); - form = $(e.target).closest('.js-discussion-note-form'); - return this.removeDiscussionNoteForm(form); + const $form = $(e.target).closest('.js-discussion-note-form'); + const $discussionNote = $(e.target).closest('.discussion-notes'); + + if ($discussionNote.length === 0) { + // Only send blur event when the discussion form + // is not part of a discussion note + const $diffFile = $form.closest('.diff-file'); + + if ($diffFile.length > 0) { + const blurEvent = new CustomEvent('blur.imageDiff', { + detail: e, + }); + + $diffFile[0].dispatchEvent(blurEvent); + } + } + + return this.removeDiscussionNoteForm($form); } /** @@ -1145,13 +1213,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; } } @@ -1414,6 +1482,15 @@ export default class Notes { // Submission successful! remove placeholder $notesContainer.find(`#${noteUniqueId}`).remove(); + const $diffFile = $form.closest('.diff-file'); + if ($diffFile.length > 0) { + const blurEvent = new CustomEvent('blur.imageDiff', { + detail: e, + }); + + $diffFile[0].dispatchEvent(blurEvent); + } + // Reset cached commands list when command is applied if (hasQuickActions) { $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); @@ -1436,7 +1513,28 @@ export default class Notes { } // Show final note element on UI - this.addDiscussionNote($form, note, $notesContainer.length === 0); + const isNewDiffComment = $notesContainer.length === 0; + this.addDiscussionNote($form, note, isNewDiffComment); + + if (isNewDiffComment) { + // Add image badge, avatar badge and toggle discussion badge for new image diffs + const notePosition = $form.find('#note_position').val(); + if ($diffFile.length > 0 && notePosition.length > 0) { + const { x, y, width, height } = JSON.parse(notePosition); + const addBadgeEvent = new CustomEvent('addBadge.imageDiff', { + detail: { + x, + y, + width, + height, + noteId: `note_${note.id}`, + discussionId: note.discussion_id, + }, + }); + + $diffFile[0].dispatchEvent(addBadgeEvent); + } + } // append flash-container to the Notes list if ($notesContainer.length) { @@ -1457,6 +1555,16 @@ export default class Notes { // Submission failed, remove placeholder note and show Flash error message $notesContainer.find(`#${noteUniqueId}`).remove(); + const blurEvent = new CustomEvent('blur.imageDiff', { + detail: e, + }); + + const closestDiffFile = $form.closest('.diff-file'); + + if (closestDiffFile.length) { + closestDiffFile[0].dispatchEvent(blurEvent); + } + if (hasQuickActions) { $notesContainer.find(`#${systemNoteUniqueId}`).remove(); } @@ -1500,6 +1608,8 @@ export default class Notes { const $noteBody = $editingNote.find('.js-task-list-container'); const $noteBodyText = $noteBody.find('.note-text'); const { formData, formContent, formAction } = this.getFormData($form); + const $diffFile = $form.closest('.diff-file'); + const $notesContainer = $form.closest('.notes'); // Cache original comment content const cachedNoteBodyText = $noteBodyText.html(); diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 1a7da84a424..2ce52e4538a 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,16 +1,19 @@ <script> - /* global Flash, Autosave */ + /* global Autosave */ import { mapActions, mapGetters } from 'vuex'; import _ from 'underscore'; import autosize from 'vendor/autosize'; + import Flash from '../../flash'; import '../../autosave'; import TaskList from '../../task_list'; import * as constants from '../constants'; import eventHub from '../event_hub'; - import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue'; + import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; + import issueDiscussionLockedWidget from './issue_discussion_locked_widget.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import issuableStateMixin from '../mixins/issuable_state'; export default { name: 'issueCommentForm', @@ -26,8 +29,9 @@ }; }, components: { - confidentialIssue, + issueWarning, issueNoteSignedOutWidget, + issueDiscussionLockedWidget, markdownField, userAvatarLink, }, @@ -55,6 +59,9 @@ isIssueOpen() { return this.issueState === constants.OPENED || this.issueState === constants.REOPENED; }, + canCreateNote() { + return this.getIssueData.current_user.can_create_note; + }, issueActionButtonTitle() { if (this.note.length) { const actionText = this.isIssueOpen ? 'close' : 'reopen'; @@ -90,9 +97,6 @@ endpoint() { return this.getIssueData.create_note_path; }, - isConfidentialIssue() { - return this.getIssueData.confidential; - }, }, methods: { ...mapActions([ @@ -142,7 +146,7 @@ Flash( 'Something went wrong while adding your comment. Please try again.', 'alert', - $(this.$refs.commentForm), + this.$refs.commentForm, ); } } else { @@ -157,7 +161,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(); }); @@ -220,6 +224,9 @@ }); }, }, + mixins: [ + issuableStateMixin, + ], mounted() { // jQuery is needed here because it is a custom event being dispatched with jQuery. $(document).on('issuable:change', (e, isClosed) => { @@ -235,6 +242,7 @@ <template> <div> <issue-note-signed-out-widget v-if="!isLoggedIn" /> + <issue-discussion-locked-widget v-else-if="!canCreateNote" /> <ul v-else class="notes notes-form timeline"> @@ -253,15 +261,22 @@ <div class="timeline-content timeline-content-form"> <form ref="commentForm" - class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"> - <confidentialIssue v-if="isConfidentialIssue" /> + class="new-note js-quick-submit common-note-form gfm-form js-main-target-form" + > + <div class="error-alert"></div> + + <issue-warning + v-if="hasWarning(getIssueData)" + :is-locked="isLocked(getIssueData)" + :is-confidential="isConfidential(getIssueData)" + /> + <markdown-field :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" :add-spacing-classes="false" - :is-confidential-issue="isConfidentialIssue" ref="markdownField"> <textarea id="note-body" diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index b131ef4b182..baf43190d9e 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'; @@ -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_discussion_locked_widget.vue b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue new file mode 100644 index 00000000000..e73ec2aaf71 --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue @@ -0,0 +1,19 @@ +<script> + export default { + computed: { + lockIcon() { + return gl.utils.spriteIcon('lock'); + }, + }, + }; + +</script> + +<template> + <div class="disabled-comment text-center"> + <span class="issuable-note-warning"> + <span class="icon" v-html="lockIcon"></span> + <span>This issue is locked. Only <b>project members</b> can comment.</span> + </span> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 1f43b8a16ad..0ddbd672bed 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(); }); 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_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 626c0f2ce18..e2539d6b89d 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -1,8 +1,9 @@ <script> import { mapGetters } from 'vuex'; import eventHub from '../event_hub'; - import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue'; + import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; + import issuableStateMixin from '../mixins/issuable_state'; export default { name: 'issueNoteForm', @@ -39,12 +40,13 @@ }; }, components: { - confidentialIssue, + issueWarning, markdownField, }, computed: { ...mapGetters([ 'getDiscussionLastNote', + 'getIssueData', 'getIssueDataByProp', 'getNotesDataByProp', 'getUserDataByProp', @@ -67,9 +69,6 @@ isDisabled() { return !this.note.length || this.isSubmitting; }, - isConfidentialIssue() { - return this.getIssueDataByProp('confidential'); - }, }, methods: { handleUpdate() { @@ -95,6 +94,9 @@ this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note); }, }, + mixins: [ + issuableStateMixin, + ], mounted() { this.$refs.textarea.focus(); }, @@ -125,7 +127,13 @@ <div class="flash-container timeline-content"></div> <form class="edit-note common-note-form js-quick-submit gfm-form"> - <confidentialIssue v-if="isConfidentialIssue" /> + + <issue-warning + v-if="hasWarning(getIssueData)" + :is-locked="isLocked(getIssueData)" + :is-confidential="isConfidential(getIssueData)" + /> + <markdown-field :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index b6fc5e5036f..aecd1f957e5 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -1,6 +1,6 @@ <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'; diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js new file mode 100644 index 00000000000..97f3ea0d5de --- /dev/null +++ b/app/assets/javascripts/notes/mixins/issuable_state.js @@ -0,0 +1,15 @@ +export default { + methods: { + isConfidential(issue) { + return !!issue.confidential; + }, + + isLocked(issue) { + return !!issue.discussion_locked; + }, + + hasWarning(issue) { + return this.isConfidential(issue) || this.isLocked(issue); + }, + }, +}; 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/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 76b97af39f1..9da0aac50a1 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -72,6 +72,13 @@ :title="pipeline.yaml_errors"> yaml invalid </span> + <span + v-if="pipeline.flags.failure_reason" + v-tooltip + class="js-pipeline-url-failure label label-danger" + :title="pipeline.failure_reason"> + error + </span> <a v-if="pipeline.flags.auto_devops" tabindex="0" 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/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index a4a27247406..1a7a5c2a415 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -13,7 +13,7 @@ * 4. Commit widget */ -/* global Flash */ +import Flash from '../../flash'; import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; 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 new file mode 100644 index 00000000000..b2b34cb83e1 --- /dev/null +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -0,0 +1,146 @@ +<script> + import popupDialog from '../../../vue_shared/components/popup_dialog.vue'; + import { __, s__, sprintf } from '../../../locale'; + import csrf from '../../../lib/utils/csrf'; + + export default { + props: { + actionUrl: { + type: String, + required: true, + }, + confirmWithPassword: { + type: Boolean, + required: true, + }, + username: { + type: String, + required: true, + }, + }, + data() { + return { + enteredPassword: '', + enteredUsername: '', + isOpen: false, + }; + }, + components: { + popupDialog, + }, + computed: { + csrfToken() { + return csrf.token; + }, + inputLabel() { + let confirmationValue; + if (this.confirmWithPassword) { + confirmationValue = __('password'); + } else { + confirmationValue = __('username'); + } + + confirmationValue = `<code>${confirmationValue}</code>`; + + return sprintf( + s__('Profiles|Type your %{confirmationValue} to confirm:'), + { confirmationValue }, + false, + ); + }, + text() { + return sprintf( + s__(`Profiles| +You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. +Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), + { + yourAccount: `<strong>${s__('Profiles|your account')}</strong>`, + deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`, + }, + false, + ); + }, + }, + methods: { + canSubmit() { + if (this.confirmWithPassword) { + return this.enteredPassword !== ''; + } + + return this.enteredUsername === this.username; + }, + onSubmit(status) { + if (status) { + if (!this.canSubmit()) { + return; + } + + this.$refs.form.submit(); + } + + this.toggleOpen(false); + }, + toggleOpen(isOpen) { + this.isOpen = isOpen; + }, + }, + }; +</script> + +<template> + <div> + <popup-dialog + v-if="isOpen" + :title="s__('Profiles|Delete your account?')" + :text="text" + :kind="`danger ${!canSubmit() && 'disabled'}`" + :primary-button-label="s__('Profiles|Delete account')" + @toggle="toggleOpen" + @submit="onSubmit"> + + <template slot="body" scope="props"> + <p v-html="props.text"></p> + + <form + ref="form" + :action="actionUrl" + method="post"> + + <input + type="hidden" + name="_method" + value="delete" /> + <input + type="hidden" + name="authenticity_token" + :value="csrfToken" /> + + <p id="input-label" v-html="inputLabel"></p> + + <input + v-if="confirmWithPassword" + name="password" + class="form-control" + type="password" + v-model="enteredPassword" + aria-labelledby="input-label" /> + <input + v-else + name="username" + class="form-control" + type="text" + v-model="enteredUsername" + aria-labelledby="input-label" /> + </form> + </template> + + </popup-dialog> + + <button + type="button" + class="btn btn-danger" + @click="toggleOpen(true)"> + {{ s__('Profiles|Delete account') }} + </button> + </div> +</template> diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js new file mode 100644 index 00000000000..635056e0eeb --- /dev/null +++ b/app/assets/javascripts/profile/account/index.js @@ -0,0 +1,21 @@ +import Vue from 'vue'; + +import deleteAccountModal from './components/delete_account_modal.vue'; + +const deleteAccountModalEl = document.getElementById('delete-account-modal'); +// eslint-disable-next-line no-new +new Vue({ + el: deleteAccountModalEl, + components: { + deleteAccountModal, + }, + render(createElement) { + return createElement('delete-account-modal', { + props: { + actionUrl: deleteAccountModalEl.dataset.actionUrl, + confirmWithPassword: !!deleteAccountModalEl.dataset.confirmWithPassword, + username: deleteAccountModalEl.dataset.username, + }, + }); + }, +}); 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/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index 10da3783123..0a9fdb074e5 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -1,15 +1,22 @@ +import _ from 'underscore'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; import ProtectedBranchDropdown from './protected_branch_dropdown'; +import AccessorUtilities from '../lib/utils/accessor'; + +const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults'; export default class ProtectedBranchCreate { constructor() { this.$form = $('.js-new-protected-branch'); + this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); + this.currentProjectUserDefaults = {}; this.buildDropdowns(); } buildDropdowns() { const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge'); const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push'); + const $protectedBranchDropdown = this.$form.find('.js-protected-branch-select'); // Cache callback this.onSelectCallback = this.onSelect.bind(this); @@ -28,15 +35,13 @@ export default class ProtectedBranchCreate { onSelect: this.onSelectCallback, }); - // Select default - $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); - $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0); - // Protected branch dropdown this.protectedBranchDropdown = new ProtectedBranchDropdown({ - $dropdown: this.$form.find('.js-protected-branch-select'), + $dropdown: $protectedBranchDropdown, onSelect: this.onSelectCallback, }); + + this.loadPreviousSelection($allowedToMergeDropdown.data('glDropdown'), $allowedToPushDropdown.data('glDropdown')); } // This will run after clicked callback @@ -45,7 +50,41 @@ export default class ProtectedBranchCreate { const $branchInput = this.$form.find('input[name="protected_branch[name]"]'); const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); + const completedForm = !( + $branchInput.val() && + $allowedToMergeInput.length && + $allowedToPushInput.length + ); + + this.savePreviousSelection($allowedToMergeInput.val(), $allowedToPushInput.val()); + this.$form.find('input[type="submit"]').attr('disabled', completedForm); + } + + loadPreviousSelection(mergeDropdown, pushDropdown) { + let mergeIndex = 0; + let pushIndex = 0; + if (this.isLocalStorageAvailable) { + const savedDefaults = JSON.parse(window.localStorage.getItem(PB_LOCAL_STORAGE_KEY)); + if (savedDefaults != null) { + mergeIndex = _.findLastIndex(mergeDropdown.fullData.roles, { + id: parseInt(savedDefaults.mergeSelection, 0), + }); + pushIndex = _.findLastIndex(pushDropdown.fullData.roles, { + id: parseInt(savedDefaults.pushSelection, 0), + }); + } + } + mergeDropdown.selectRowAtIndex(mergeIndex); + pushDropdown.selectRowAtIndex(pushIndex); + } - this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length)); + savePreviousSelection(mergeSelection, pushSelection) { + if (this.isLocalStorageAvailable) { + const branchDefaults = { + mergeSelection, + pushSelection, + }; + window.localStorage.setItem(PB_LOCAL_STORAGE_KEY, JSON.stringify(branchDefaults)); + } } } 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/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue index d6c864cb976..cc60aa5939c 100644 --- a/app/assets/javascripts/repo/components/repo.vue +++ b/app/assets/javascripts/repo/components/repo.vue @@ -62,7 +62,7 @@ export default { :primary-button-label="__('Discard changes')" kind="warning" :title="__('Are you sure?')" - :body="__('Are you sure you want to discard your changes?')" + :text="__('Are you sure you want to discard your changes?')" @toggle="toggleDialogOpen" @submit="dialogSubmitted" /> diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index 119e38c583d..6d8cc964eb2 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -1,5 +1,5 @@ <script> -/* global Flash */ +import Flash from '../../flash'; import Store from '../stores/repo_store'; import RepoMixin from '../mixins/repo_mixin'; import Service from '../services/repo_service'; diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index 96d6a75bb61..02d9c775046 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -63,12 +63,7 @@ const RepoEditor = { const lineNumber = e.target.position.lineNumber; if (e.target.element.classList.contains('line-numbers')) { location.hash = `L${lineNumber}`; - Store.activeLine = lineNumber; - - Helper.monacoInstance.setPosition({ - lineNumber: this.activeLine, - column: 1, - }); + Store.setActiveLine(lineNumber); } }, }, @@ -101,6 +96,15 @@ const RepoEditor = { this.setupEditor(); } }, + + activeLine() { + if (Helper.monacoInstance) { + Helper.monacoInstance.setPosition({ + lineNumber: this.activeLine, + column: 1, + }); + } + }, }, computed: { shouldHideEditor() { diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index 2fe369a4693..a87bef6084a 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -14,6 +14,11 @@ export default { highlightFile() { $(this.$el).find('.file-content').syntaxHighlight(); }, + highlightLine() { + if (Store.activeLine > -1) { + this.lineHighlighter.highlightHash(`#L${Store.activeLine}`); + } + }, }, mounted() { this.highlightFile(); @@ -26,8 +31,12 @@ export default { html() { this.$nextTick(() => { this.highlightFile(); + this.highlightLine(); }); }, + activeLine() { + this.highlightLine(); + }, }, }; </script> diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index 1e40814b95f..e0f3c33003a 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -18,22 +18,40 @@ export default { }, created() { - this.addPopEventListener(); + window.addEventListener('popstate', this.checkHistory); + }, + destroyed() { + window.removeEventListener('popstate', this.checkHistory); }, data: () => Store, methods: { - addPopEventListener() { - window.addEventListener('popstate', () => { - if (location.href.indexOf('#') > -1) return; - this.linkClicked({ + 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) { + fileClicked(clickedFile, lineNumber) { let file = clickedFile; if (file.loading) return; file.loading = true; @@ -41,17 +59,20 @@ export default { 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); } @@ -74,8 +95,8 @@ export default { <thead v-if="!isMini"> <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 class="hidden-sm hidden-xs last-commit">Last commit</th> + <th class="hidden-xs last-update text-right">Last update</th> </tr> </thead> <tbody> diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index ac59a2bed23..46204598e1d 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -1,7 +1,6 @@ -/* global Flash */ import Service from '../services/repo_service'; import Store from '../stores/repo_store'; -import '../../flash'; +import Flash from '../../flash'; const RepoHelper = { monacoInstance: null, @@ -254,7 +253,9 @@ const RepoHelper = { RepoHelper.key = RepoHelper.genKey(); - history.pushState({ key: RepoHelper.key }, '', url); + if (document.location.pathname !== url) { + history.pushState({ key: RepoHelper.key }, '', url); + } if (title) { document.title = title; diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js index af83497fa39..830685f7e6e 100644 --- a/app/assets/javascripts/repo/services/repo_service.js +++ b/app/assets/javascripts/repo/services/repo_service.js @@ -1,4 +1,3 @@ -/* global Flash */ import axios from 'axios'; import Store from '../stores/repo_store'; import Api from '../../api'; diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index 9a4fc40bc69..c633f538c1b 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -1,4 +1,3 @@ -/* global Flash */ import Helper from '../helpers/repo_helper'; import Service from '../services/repo_service'; @@ -26,7 +25,7 @@ const RepoStore = { }, activeFile: Helper.getDefaultActiveFile(), activeFileIndex: 0, - activeLine: 0, + activeLine: -1, activeFileLabel: 'Raw', files: [], isCommitable: false, @@ -85,6 +84,7 @@ const RepoStore = { if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name); RepoStore.binary = file.binary; + RepoStore.setActiveLine(-1); }, setFileActivity(file, openedFile, i) { @@ -101,6 +101,10 @@ const RepoStore = { 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. 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/shortcuts.js b/app/assets/javascripts/shortcuts.js index e754f6c4460..f63b99ba1c5 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -18,23 +18,8 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; Mousetrap.bind('f', (e => this.focusFilter(e))); Mousetrap.bind('p b', this.onTogglePerfBar); - const $globalDropdownMenu = $('.global-dropdown-menu'); - const $globalDropdownToggle = $('.global-dropdown-toggle'); const findFileURL = document.body.dataset.findFile; - $('.global-dropdown').on('hide.bs.dropdown', () => { - $globalDropdownMenu.removeClass('shortcuts'); - }); - - Mousetrap.bind('n', () => { - $globalDropdownMenu.toggleClass('shortcuts'); - $globalDropdownToggle.trigger('click'); - - if (!$globalDropdownMenu.is(':visible')) { - $globalDropdownToggle.blur(); - } - }); - Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos')); Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity')); Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues')); 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 8e7abdbffef..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 { @@ -47,9 +47,9 @@ export default { </script> <template> - <div class="block confidentiality"> + <div class="block issuable-sidebar-item confidentiality"> <div class="sidebar-collapsed-icon"> - <i class="fa" :class="faEye" aria-hidden="true" data-hidden="true"></i> + <i class="fa" :class="faEye" aria-hidden="true"></i> </div> <div class="title hide-collapsed"> Confidentiality @@ -62,19 +62,19 @@ export default { Edit </a> </div> - <div class="value confidential-value hide-collapsed"> + <div class="value sidebar-item-value hide-collapsed"> <editForm v-if="edit" :toggle-form="toggleForm" :is-confidential="isConfidential" :update-confidential-attribute="updateConfidentialAttribute" /> - <div v-if="!isConfidential" class="no-value confidential-value"> - <i class="fa fa-eye is-not-confidential"></i> + <div v-if="!isConfidential" class="no-value sidebar-item-value"> + <i class="fa fa-eye sidebar-item-icon"></i> Not confidential </div> - <div v-else class="value confidential-value hide-collapsed"> - <i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i> + <div v-else class="value sidebar-item-value hide-collapsed"> + <i aria-hidden="true" class="fa fa-eye-slash sidebar-item-icon is-active"></i> This issue is confidential </div> </div> diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue index d578b663a54..dd17b5abd46 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue @@ -2,9 +2,6 @@ import editFormButtons from './edit_form_buttons.vue'; export default { - components: { - editFormButtons, - }, props: { isConfidential: { required: true, @@ -19,12 +16,16 @@ export default { type: Function, }, }, + + components: { + editFormButtons, + }, }; </script> <template> <div class="dropdown open"> - <div class="dropdown-menu confidential-warning-message"> + <div class="dropdown-menu sidebar-item-warning-message"> <div> <p v-if="!isConfidential"> You are going to turn on the confidentiality. This means that only team members with diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue index 97af4a3f505..7ed0619ee6b 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue @@ -15,7 +15,7 @@ export default { }, }, computed: { - onOrOff() { + toggleButtonText() { return this.isConfidential ? 'Turn Off' : 'Turn On'; }, updateConfidentialBool() { @@ -26,7 +26,7 @@ export default { </script> <template> - <div class="confidential-warning-message-actions"> + <div class="sidebar-item-warning-message-actions"> <button type="button" class="btn btn-default append-right-10" @@ -39,7 +39,7 @@ export default { class="btn btn-close" @click.prevent="updateConfidentialAttribute(updateConfidentialBool)" > - {{ onOrOff }} + {{ toggleButtonText }} </button> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue new file mode 100644 index 00000000000..c7a6edc7c70 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue @@ -0,0 +1,61 @@ +<script> +import editFormButtons from './edit_form_buttons.vue'; +import issuableMixin from '../../../vue_shared/mixins/issuable'; + +export default { + props: { + isLocked: { + required: true, + type: Boolean, + }, + + toggleForm: { + required: true, + type: Function, + }, + + updateLockedAttribute: { + required: true, + type: Function, + }, + + issuableType: { + required: true, + type: String, + }, + }, + + mixins: [ + issuableMixin, + ], + + components: { + editFormButtons, + }, +}; +</script> + +<template> + <div class="dropdown open"> + <div class="dropdown-menu sidebar-item-warning-message"> + <p class="text" v-if="isLocked"> + Unlock this {{ issuableDisplayName(issuableType) }}? + <strong>Everyone</strong> + will be able to comment. + </p> + + <p class="text" v-else> + Lock this {{ issuableDisplayName(issuableType) }}? + Only + <strong>project members</strong> + will be able to comment. + </p> + + <edit-form-buttons + :is-locked="isLocked" + :toggle-form="toggleForm" + :update-locked-attribute="updateLockedAttribute" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue new file mode 100644 index 00000000000..c3a553a7605 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue @@ -0,0 +1,50 @@ +<script> +export default { + props: { + isLocked: { + required: true, + type: Boolean, + }, + + toggleForm: { + required: true, + type: Function, + }, + + updateLockedAttribute: { + required: true, + type: Function, + }, + }, + + computed: { + buttonText() { + return this.isLocked ? this.__('Unlock') : this.__('Lock'); + }, + + toggleLock() { + return !this.isLocked; + }, + }, +}; +</script> + +<template> + <div class="sidebar-item-warning-message-actions"> + <button + type="button" + class="btn btn-default append-right-10" + @click="toggleForm" + > + {{ __('Cancel') }} + </button> + + <button + type="button" + class="btn btn-close" + @click.prevent="updateLockedAttribute(toggleLock)" + > + {{ buttonText }} + </button> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue new file mode 100644 index 00000000000..c4b2900e020 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -0,0 +1,120 @@ +<script> +/* global Flash */ +import editForm from './edit_form.vue'; +import issuableMixin from '../../../vue_shared/mixins/issuable'; + +export default { + props: { + isLocked: { + required: true, + type: Boolean, + }, + + isEditable: { + required: true, + type: Boolean, + }, + + mediator: { + required: true, + type: Object, + validator(mediatorObject) { + return mediatorObject.service && mediatorObject.service.update && mediatorObject.store; + }, + }, + + issuableType: { + required: true, + type: String, + }, + }, + + mixins: [ + issuableMixin, + ], + + components: { + editForm, + }, + + computed: { + lockIconClass() { + return this.isLocked ? 'fa-lock' : 'fa-unlock'; + }, + + isLockDialogOpen() { + return this.mediator.store.isLockDialogOpen; + }, + }, + + methods: { + toggleForm() { + this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; + }, + + updateLockedAttribute(locked) { + this.mediator.service.update(this.issuableType, { + discussion_locked: locked, + }) + .then(() => location.reload()) + .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}`))); + }, + }, +}; +</script> + +<template> + <div class="block issuable-sidebar-item lock"> + <div class="sidebar-collapsed-icon"> + <i + class="fa" + :class="lockIconClass" + aria-hidden="true" + ></i> + </div> + + <div class="title hide-collapsed"> + Lock {{issuableDisplayName(issuableType) }} + <button + v-if="isEditable" + class="pull-right lock-edit btn btn-blank" + type="button" + @click.prevent="toggleForm" + > + {{ __('Edit') }} + </button> + </div> + + <div class="value sidebar-item-value hide-collapsed"> + <edit-form + v-if="isLockDialogOpen" + :toggle-form="toggleForm" + :is-locked="isLocked" + :update-locked-attribute="updateLockedAttribute" + :issuable-type="issuableType" + /> + + <div + v-if="isLocked" + class="value sidebar-item-value" + > + <i + aria-hidden="true" + class="fa fa-lock sidebar-item-icon is-active" + ></i> + {{ __('Locked') }} + </div> + + <div + v-else + class="no-value sidebar-item-value hide-collapsed" + > + <i + aria-hidden="true" + class="fa fa-unlock sidebar-item-icon" + ></i> + {{ __('Unlocked') }} + </div> + </div> + </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/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js index 3d8972050a9..09b9d75c02d 100644 --- a/app/assets/javascripts/sidebar/sidebar_bundle.js +++ b/app/assets/javascripts/sidebar/sidebar_bundle.js @@ -1,46 +1,76 @@ import Vue from 'vue'; -import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking'; -import sidebarAssignees from './components/assignees/sidebar_assignees'; -import confidential from './components/confidential/confidential_issue_sidebar.vue'; +import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking'; +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 Translate from '../vue_shared/translate'; import Mediator from './sidebar_mediator'; +Vue.use(Translate); + +function mountConfidentialComponent(mediator) { + const el = document.getElementById('js-confidential-entry-point'); + + if (!el) return; + + const dataNode = document.getElementById('js-confidential-issue-data'); + const initialData = JSON.parse(dataNode.innerHTML); + + const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar); + + new ConfidentialComp({ + propsData: { + isConfidential: initialData.is_confidential, + isEditable: initialData.is_editable, + service: mediator.service, + }, + }).$mount(el); +} + +function mountLockComponent(mediator) { + const el = document.getElementById('js-lock-entry-point'); + + if (!el) return; + + const dataNode = document.getElementById('js-lock-issue-data'); + const initialData = JSON.parse(dataNode.innerHTML); + + const LockComp = Vue.extend(LockIssueSidebar); + + new LockComp({ + propsData: { + isLocked: initialData.is_locked, + isEditable: initialData.is_editable, + mediator, + issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request', + }, + }).$mount(el); +} + function domContentLoaded() { const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); const mediator = new Mediator(sidebarOptions); mediator.fetch(); - const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees'); - const confidentialEl = document.querySelector('#js-confidential-entry-point'); + const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees'); // Only create the sidebarAssignees vue app if it is found in the DOM // We currently do not use sidebarAssignees for the MR page if (sidebarAssigneesEl) { - new Vue(sidebarAssignees).$mount(sidebarAssigneesEl); + new Vue(SidebarAssignees).$mount(sidebarAssigneesEl); } - if (confidentialEl) { - const dataNode = document.getElementById('js-confidential-issue-data'); - const initialData = JSON.parse(dataNode.innerHTML); + mountConfidentialComponent(mediator); + mountLockComponent(mediator); - const ConfidentialComp = Vue.extend(confidential); - - new ConfidentialComp({ - propsData: { - isConfidential: initialData.is_confidential, - isEditable: initialData.is_editable, - service: mediator.service, - }, - }).$mount(confidentialEl); - - new SidebarMoveIssue( - mediator, - $('.js-move-issue'), - $('.js-move-issue-confirmation-button'), - ).init(); - } + new SidebarMoveIssue( + mediator, + $('.js-move-issue'), + $('.js-move-issue-confirmation-button'), + ).init(); - new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker'); + new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker'); } document.addEventListener('DOMContentLoaded', domContentLoaded); diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index 2fe6e5b31f0..ede3a0de144 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'; diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js index cc04a2a3fcf..d5d04103f3f 100644 --- a/app/assets/javascripts/sidebar/stores/sidebar_store.js +++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js @@ -15,6 +15,7 @@ export default class SidebarStore { }; this.autocompleteProjects = []; this.moveToProjectId = 0; + this.isLockDialogOpen = false; SidebarStore.singleton = this; } diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 4505a79a2df..3f811c59cb9 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ import FilesCommentButton from './files_comment_button'; +import imageDiffHelper from './image_diff/helpers/index'; const WRAPPER = '<div class="diff-content"></div>'; const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; @@ -74,7 +75,11 @@ export default class SingleFileDiff { gl.diffNotesCompileComponents(); } - FilesCommentButton.init($(_this.file)); + const $file = $(_this.file); + FilesCommentButton.init($file); + + const canCreateNote = $file.closest('.files').is('[data-can-create-note]'); + imageDiffHelper.initImageDiff($file[0], canCreateNote); if (cb) cb(); }; diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 3a06b477d7c..77db075d1ef 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,6 +1,5 @@ /* 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 { 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/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/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/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 0c48a484fe8..b8a96b23012 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'; @@ -38,24 +38,40 @@ export default { return this.useCommitMessageWithDescription ? withoutDesc : withDesc; }, - mergeButtonClass() { - const defaultClass = 'btn btn-sm btn-success accept-merge-request'; - const failedClass = `${defaultClass} btn-danger`; - const inActionClass = `${defaultClass} btn-info`; + status() { const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; if (hasCI && !ciStatus) { - return failedClass; + return 'failed'; } else if (!pipeline) { - return defaultClass; + return 'success'; } else if (isPipelineActive) { - return inActionClass; + return 'pending'; } else if (isPipelineFailed) { + return 'failed'; + } + + return 'success'; + }, + mergeButtonClass() { + const defaultClass = 'btn btn-sm btn-success accept-merge-request'; + const failedClass = `${defaultClass} btn-danger`; + const inActionClass = `${defaultClass} btn-info`; + + if (this.status === 'failed') { return failedClass; + } else if (this.status === 'pending') { + return inActionClass; } return defaultClass; }, + iconClass() { + if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) { + return 'failed'; + } + return 'success'; + }, mergeButtonText() { if (this.isMergingImmediately) { return 'Merge in progress'; @@ -84,13 +100,8 @@ export default { }, }, methods: { - isMergeAllowed() { - return !this.mr.onlyAllowMergeIfPipelineSucceeds || - this.mr.isPipelinePassing || - this.mr.isPipelineSkipped; - }, shouldShowMergeControls() { - return this.isMergeAllowed() || this.shouldShowMergeWhenPipelineSucceedsText; + return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText; }, updateCommitMessage() { const cmwd = this.mr.commitMessageWithDescription; @@ -209,7 +220,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="success" /> + <status-icon :status="iconClass" /> <div class="media-body"> <div class="mr-widget-body-controls media space-children"> <span class="btn-group append-bottom-5"> 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..4f497b204a3 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,4 @@ -/* global Flash */ - +import Flash from '../flash'; import { WidgetHeader, WidgetMergeHelp, diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index e554082149b..c1f7e64f580 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -73,6 +73,7 @@ export default class MergeRequestStore { this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path; this.hasSHAChanged = this.sha !== data.diff_head_sha; this.canBeMerged = data.can_be_merged || false; + this.isMergeAllowed = data.mergeable || false; this.mergeOngoing = data.merge_ongoing; // Cherry-pick and Revert actions related diff --git a/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue deleted file mode 100644 index 397d16331d5..00000000000 --- a/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue +++ /dev/null @@ -1,16 +0,0 @@ -<script> - export default { - name: 'confidentialIssueWarning', - }; -</script> -<template> - <div class="confidential-issue-warning"> - <i - aria-hidden="true" - class="fa fa-eye-slash"> - </i> - <span> - This is a confidential issue. Your comment will not be visible to the public. - </span> - </div> -</template> diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue new file mode 100644 index 00000000000..16c0a8efcd2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -0,0 +1,55 @@ +<script> + export default { + props: { + isLocked: { + type: Boolean, + default: false, + required: false, + }, + + isConfidential: { + type: Boolean, + default: false, + required: false, + }, + }, + + computed: { + iconClass() { + return { + 'fa-eye-slash': this.isConfidential, + 'fa-lock': this.isLocked, + }; + }, + + isLockedAndConfidential() { + return this.isConfidential && this.isLocked; + }, + }, + }; +</script> +<template> + <div class="issuable-note-warning"> + <i + aria-hidden="true" + class="fa icon" + :class="iconClass" + v-if="!isLockedAndConfidential" + ></i> + + <span v-if="isLockedAndConfidential"> + {{ __('This issue is confidential and locked.') }} + {{ __('People without permission will never get a notification and won\'t be able to comment.') }} + </span> + + <span v-else-if="isConfidential"> + {{ __('This is a confidential issue.') }} + {{ __('Your comment will not be visible to the public.') }} + </span> + + <span v-else-if="isLocked"> + {{ __('This issue is locked.') }} + {{ __('Only project members can comment.') }} + </span> + </div> +</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..af4187fab46 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -1,5 +1,5 @@ <script> - /* global Flash */ + import Flash from '../../../flash'; import markdownHeader from './header.vue'; import markdownToolbar from './toolbar.vue'; diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index 994b33bc1c9..9279b50cd55 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -7,7 +7,7 @@ export default { type: String, required: true, }, - body: { + text: { type: String, required: true, }, @@ -63,7 +63,9 @@ export default { <h4 class="modal-title">{{this.title}}</h4> </div> <div class="modal-body"> - <p>{{this.body}}</p> + <slot name="body" :text="text"> + <p>{{text}}</p> + </slot> </div> <div class="modal-footer"> <button diff --git a/app/assets/javascripts/vue_shared/mixins/issuable.js b/app/assets/javascripts/vue_shared/mixins/issuable.js new file mode 100644 index 00000000000..263361587e0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/mixins/issuable.js @@ -0,0 +1,9 @@ +export default { + methods: { + issuableDisplayName(issuableType) { + const displayName = issuableType.replace(/_/, ' '); + + return this.__ ? this.__(displayName) : displayName; + }, + }, +}; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 74b846217bb..c7be94e2c8e 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -30,16 +30,16 @@ @import "framework/media_object"; @import "framework/mobile"; @import "framework/modal"; -@import "framework/nav"; -@import "framework/new-nav"; @import "framework/pagination"; @import "framework/panels"; +@import "framework/secondary-navigation-elements"; @import "framework/selects"; @import "framework/sidebar"; @import "framework/new-sidebar"; @import "framework/tables"; @import "framework/notes"; @import "framework/timeline"; +@import "framework/tooltips"; @import "framework/typography"; @import "framework/zen"; @import "framework/blank"; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 667b73e150d..f0e6b23757f 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -115,8 +115,7 @@ @return $unfoldedTransition; } -.btn, -.global-dropdown-toggle { +.btn { @include transition(background-color, border-color, color, box-shadow); } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 1d72a70f0f5..dbd990f84c1 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -207,6 +207,16 @@ &.user-cover-block { padding: 24px 0 0; + + .nav-links { + justify-content: center; + width: 100%; + float: none; + + &.scrolling-tabs { + float: none; + } + } } .group-info { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index d178bc17462..b131e2d57ee 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -1,3 +1,25 @@ +@mixin btn-comment-icon { + border-radius: 50%; + background: $white-light; + padding: 1px 5px; + font-size: 12px; + color: $blue-500; + width: 23px; + height: 23px; + border: 1px solid $blue-500; + + &:hover, + &.inverted { + background: $blue-500; + border-color: $blue-600; + color: $white-light; + } + + &:active { + outline: 0; + } +} + @mixin btn-default { border-radius: 3px; font-size: $gl-font-size; @@ -381,7 +403,11 @@ background: transparent; border: 0; + &:hover, + &:active, &:focus { outline: 0; + background: transparent; + box-shadow: none; } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index fa92d4ccf4f..5b950ae0ba0 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -749,7 +749,7 @@ margin-bottom: $dropdown-vertical-offset; } - li { + li:not(.dropdown-bold-header) { display: block; padding: 0 1px; @@ -889,7 +889,7 @@ @include new-style-dropdown('.breadcrumbs-list .dropdown '); @include new-style-dropdown('.js-namespace-select + '); -header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu { +header.header-content .dropdown-menu.projects-dropdown-menu { padding: 0; } diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 6b69e8018be..52b87de7a3d 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -5,7 +5,7 @@ @mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) { // Header - header.navbar-gitlab-new { + .navbar-gitlab { background-color: $color-900; .navbar-collapse { @@ -95,7 +95,7 @@ } } - .title { + .navbar .title { > a { &:hover, &:focus { @@ -200,9 +200,9 @@ body { &.ui_light { @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700); - header.navbar-gitlab-new { + .navbar-gitlab { background-color: $theme-gray-100; - box-shadow: 0 2px 0 0 $border-color; + box-shadow: 0 1px 0 0 $border-color; .logo-text svg { fill: $theme-gray-900; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index d932ea8794f..22945e935ef 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -1,112 +1,37 @@ -/* - * Application Header - * - */ +.content-wrapper.page-with-new-nav { + margin-top: $header-height; +} -header { +.navbar-gitlab { @include new-style-dropdown; - transition: padding $sidebar-transition-duration; - - &.navbar-empty { - height: $header-height; - background: $white-light; - border-bottom: 1px solid $white-normal; - - .center-logo { - margin: 8px 0; - text-align: center; - - .tanuki-logo, - img { - height: 36px; - } - } - } - &.navbar-gitlab { padding: 0 16px; z-index: 1000; margin-bottom: 0; min-height: $header-height; - background-color: $gray-light; border: none; border-bottom: 1px solid $border-color; position: fixed; top: 0; left: 0; right: 0; - color: $gl-text-color-secondary; border-radius: 0; - @media (max-width: $screen-xs-min) { - padding: 0 16px; - } - - &.with-horizontal-nav { - border-bottom: 0; + .logo-text { + line-height: initial; - .navbar-border { - height: 1px; - position: absolute; - right: 0; - left: 0; - bottom: -1px; - background-color: $border-color; - opacity: 0; + svg { + width: 55px; + height: 14px; + margin: 0; + fill: $white-light; } } .container-fluid { - width: 100% !important; - filter: none; padding: 0; - .nav > li > a { - color: currentColor; - font-size: 18px; - padding: 0; - margin: (($header-height - 28) / 2) 3px; - margin-left: 8px; - height: 28px; - min-width: 32px; - line-height: 28px; - text-align: center; - - &.header-user-dropdown-toggle { - margin-left: 14px; - - &:hover, - &:focus, - &:active { - .header-user-avatar { - border-color: rgba($avatar-border, .2); - } - } - } - - &:hover, - &:focus, - &:active { - background-color: transparent; - color: $gl-text-color; - - svg { - fill: $gl-text-color; - } - } - - .fa-caret-down { - font-size: 14px; - } - - .fa-chevron-down { - position: relative; - top: -3px; - font-size: 10px; - } - } - .user-counter { svg { margin-right: 3px; @@ -114,81 +39,117 @@ header { } .navbar-toggle { - color: $nav-toggle-gray; - margin: 5px 0; - border-radius: 0; right: -10px; - padding: 6px 10px; + border-radius: 0; + min-width: 45px; + padding: 0; + margin-right: -7px; + font-size: 14px; + text-align: center; + color: currentColor; - &:hover { - background-color: $white-normal; + &:hover, + &:focus, + &.active { + color: currentColor; + background-color: transparent; } - &.active { - color: $gl-text-color-secondary; + .more-icon, + .close-icon { + fill: $white-light; + margin: auto; } } } } - &.navbar-gitlab-new { - .close-icon { + .close-icon { + display: none; + } + + .menu-expanded { + .more-icon { display: none; } - .menu-expanded { - .more-icon { - display: none; - } - - .close-icon { - display: block; - } + .close-icon { + display: block; } } - .global-dropdown { - position: absolute; - left: -10px; + .header-content { + display: -webkit-flex; + display: flex; + justify-content: space-between; + position: relative; + min-height: $header-height; + padding-left: 0; - .badge { - font-size: 11px; + .title-container { + display: -webkit-flex; + display: flex; + -webkit-align-items: stretch; + align-items: stretch; + -webkit-flex: 1 1 auto; + flex: 1 1 auto; + padding-top: 0; + overflow: visible; } - li { - &.active a { - font-weight: $gl-font-weight-bold; + .title { + padding-right: 0; + color: currentColor; + display: -webkit-flex; + display: flex; + position: relative; + margin: 0; + font-size: 18px; + vertical-align: top; + white-space: nowrap; + + img { + height: 28px; + margin-right: 8px; } - } - } - .global-dropdown-toggle { - margin: 7px 0; - font-size: 18px; - padding: 6px 10px; - border: none; - background-color: $gray-light; + &.wrap { + white-space: normal; + } - &:hover { - background-color: $white-normal; - } + &.initializing { + opacity: 0; + } + + a { + display: -webkit-flex; + display: flex; + align-items: center; + padding: 2px 8px; + margin: 5px 2px 5px -8px; + border-radius: $border-radius-default; - &:focus { - outline: none; - background-color: $white-normal; + svg { + @media (min-width: $screen-sm-min) { + margin-right: 8px; + } + } + } + + .project-item-select { + right: auto; + left: 0; + } } - } - .header-content { - display: flex; - justify-content: space-between; - position: relative; - min-height: $header-height; - padding-left: 30px; + .dropdown.open { + > a { + border-bottom-color: $white-light; + } + } &.menu-expanded { @media (max-width: $screen-xs-max) { - .header-logo, .title-container { display: none; } @@ -198,111 +159,180 @@ header { } } } + } - .dropdown-menu { - margin-top: -5px; - } + .dropdown-bold-header { + color: $gl-text-color-secondary; + font-size: 12px; + } - .header-logo { - display: inline-block; - margin: 0 12px 0 2px; - position: relative; - top: 10px; - transition-duration: .3s; + .navbar-collapse { + flex: 0 0 auto; + border-top: none; + padding: 0; - svg, - img { - height: 28px; - } + @media (max-width: $screen-xs-max) { + flex: 1 1 auto; + } - &:hover { - cursor: pointer; + .nav { + > li:not(.hidden-xs) a { + @media (max-width: $screen-xs-max) { + margin-left: 0; + min-width: 100%; + } } } + } - .group-name-toggle { - margin: 3px 5px; - } + .container-fluid { - .group-title { - &.is-hidden { - .hidable:not(:last-of-type) { - display: none; + .navbar-nav { + @media (max-width: $screen-xs-max) { + display: -webkit-flex; + display: flex; + padding-right: 10px; + } + + li { + .badge { + box-shadow: none; + font-weight: $gl-font-weight-bold; } } } - .title-container { - display: flex; - align-items: flex-start; - flex: 1 1 auto; - padding-top: 14px; - overflow: hidden; - } + .nav > li { + &.header-user { + @media (max-width: $screen-xs-max) { + padding-left: 10px; + } + } - .title { - position: relative; - padding-right: 20px; - margin: 0; - font-size: 18px; - line-height: 22px; - display: inline-block; - font-weight: $gl-font-weight-normal; - color: $gl-text-color; - vertical-align: top; - white-space: nowrap; + > a { + will-change: color; + margin: 4px 2px; + padding: 6px 8px; + height: 32px; - &.wrap { - white-space: normal; + @media (max-width: $screen-xs-max) { + padding: 0; + } + + &.header-user-dropdown-toggle { + margin-left: 2px; + + .header-user-avatar { + margin-right: 0; + } + } + + &:hover, + &:focus { + text-decoration: none; + outline: 0; + opacity: 1; + color: $white-light; + + svg { + fill: currentColor; + } + + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $white-light; + } + } + } } - &.initializing { - opacity: 0; + .header-new-dropdown-toggle { + margin-right: 0; } - a { - color: currentColor; + .impersonated-user, + .impersonated-user:hover { + margin-right: 1px; + background-color: $white-light; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } - &:hover { - text-decoration: underline; - color: $gl-header-nav-hover-color; + .impersonation-btn, + .impersonation-btn:hover { + background-color: $white-light; + margin-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + i { + color: $orange-500; + font-size: 20px; } } - .dropdown-toggle-caret { - color: $gl-text-color; - border: transparent; - background: transparent; - position: absolute; - top: 2px; - right: 3px; - width: 12px; - line-height: 19px; - padding: 0; - font-size: 10px; - text-align: center; - cursor: pointer; + &.active > a, + &.dropdown.open > a { - &:hover { - color: $gl-header-nav-hover-color; + svg { + fill: currentColor; } } + } + } +} - .project-item-select { - right: auto; - left: 0; +.navbar-sub-nav, +.navbar-nav { + > li { + > a:hover, + > a:focus { + text-decoration: none; + outline: 0; + color: $white-light; + + svg { + fill: currentColor; } } - .navbar-collapse { - flex: 0 0 auto; - border-top: none; - padding: 0; - - @media (max-width: $screen-xs-max) { - flex: 1 1 auto; + > a { + display: -webkit-flex; + display: flex; + align-items: center; + justify-content: center; + padding: 6px 8px; + margin: 4px 2px; + font-size: 12px; + color: currentColor; + border-radius: $border-radius-default; + height: 32px; + font-weight: $gl-font-weight-bold; + + svg { + fill: currentColor; } } + + &.line-separator { + margin: 8px; + } + } +} + +.navbar-sub-nav { + display: -webkit-flex; + display: flex; + margin: 0 0 0 6px; + + .projects-dropdown-menu { + padding: 0; + } + + .dropdown-chevron { + position: relative; + top: -1px; + font-size: 10px; } .project-item-select-holder { @@ -314,8 +344,123 @@ header { } } -.with-performance-bar header.navbar-gitlab { - top: $performance-bar-height; +.caret-down { + height: 11px; + width: 11px; + margin-left: 4px; + fill: currentColor; +} + +.header-user .dropdown-menu-nav, +.header-new .dropdown-menu-nav { + margin-top: $dropdown-vertical-offset; +} + +.breadcrumbs { + display: -webkit-flex; + display: flex; + min-height: 48px; + color: $gl-text-color; +} + +.breadcrumbs-container { + display: -webkit-flex; + display: flex; + width: 100%; + position: relative; + padding-top: $gl-padding / 2; + padding-bottom: $gl-padding / 2; + align-items: center; + border-bottom: 1px solid $border-color; +} + +.breadcrumbs-links { + -webkit-flex: 1; + flex: 1; + min-width: 0; + align-self: center; + color: $gl-text-color-secondary; + + .avatar-tile { + margin-right: 4px; + border: 1px solid $border-color; + border-radius: 50%; + vertical-align: sub; + } + + .text-expander { + margin-left: 0; + margin-right: 2px; + + > i { + position: relative; + top: 1px; + } + } +} + +.breadcrumbs-list { + display: -webkit-flex; + display: flex; + flex-wrap: wrap; + margin-bottom: 0; + line-height: 16px; + + > li { + display: flex; + align-items: center; + position: relative; + padding: 2px 0; + + &:not(:last-child) { + margin-right: 20px; + } + + > a { + font-size: 12px; + color: currentColor; + } + } +} + +.breadcrumb-item-text { + @include str-truncated(128px); + text-decoration: inherit; +} + +.breadcrumbs-list-angle { + position: absolute; + right: -12px; + top: 50%; + color: $gl-text-color-tertiary; + transform: translateY(-50%); +} + +.breadcrumbs-extra { + display: -webkit-flex; + display: flex; + flex: 0 0 auto; + margin-left: auto; +} + +.breadcrumbs-sub-title { + margin: 0; + font-size: 12px; + font-weight: 600; + line-height: 16px; + + a { + color: $gl-text-color; + } +} + +.btn-sign-in { + margin-top: 3px; + font-weight: $gl-font-weight-bold; + + &:hover { + background-color: $white-light; + } } .navbar-nav { @@ -347,11 +492,10 @@ header { } @media (max-width: $screen-xs-max) { - header .container-fluid { + .navbar-gitlab .container-fluid { font-size: 18px; .navbar-nav { - display: table; table-layout: fixed; width: 100%; margin: 0; @@ -359,7 +503,8 @@ header { } .navbar-collapse { - padding-left: 5px; + margin-left: -8px; + margin-right: -10px; .nav > li:not(.hidden-xs) { display: table-cell !important; @@ -385,11 +530,11 @@ header { .dropdown-menu-nav { width: auto; min-width: 140px; - margin-top: -5px; + margin-top: 4px; color: $gl-text-color; left: auto; - .current-user { + li.current-user { padding: 5px 18px; .user-name { @@ -405,3 +550,23 @@ header { border-radius: 50%; border: 1px solid $avatar-border; } + +.with-performance-bar .navbar-gitlab { + top: $performance-bar-height; +} + +.navbar-empty { + height: $header-height; + background: $white-light; + border-bottom: 1px solid $white-normal; + + .center-logo { + margin: 8px 0; + text-align: center; + + .tanuki-logo, + img { + height: 36px; + } + } +} diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 59bfc5a8d77..6819fd88b7f 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -28,6 +28,7 @@ svg { &.s8 { @include svg-size(8px); } + &.s12 { @include svg-size(12px); } &.s16 { @include svg-size(16px); } &.s18 { @include svg-size(18px); } &.s24 { @include svg-size(24px); } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index bd521028c44..69d19ea2962 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -25,10 +25,6 @@ body { .content-wrapper { padding-bottom: 100px; - - &:not(.page-with-layout-nav) { - margin-top: $header-height; - } } .container { diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 5b581780447..1cebd02df48 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -1,10 +1,17 @@ +.modal-header { + padding: #{3 * $grid-size} #{2 * $grid-size}; + + .page-title { + margin-top: 0; + } +} + .modal-body { position: relative; - padding: 15px; + padding: #{3 * $grid-size} #{2 * $grid-size}; .form-actions { - margin: -$gl-padding + 1; - margin-top: 15px; + margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size}; } .text-danger { diff --git a/app/assets/stylesheets/framework/new-nav.scss b/app/assets/stylesheets/framework/new-nav.scss index 7899be2c2d3..e69de29bb2d 100644 --- a/app/assets/stylesheets/framework/new-nav.scss +++ b/app/assets/stylesheets/framework/new-nav.scss @@ -1,404 +0,0 @@ -@import "framework/variables"; -@import 'framework/tw_bootstrap_variables'; -@import "bootstrap/variables"; -@import "framework/mixins"; - -.content-wrapper.page-with-new-nav { - margin-top: $new-navbar-height; -} - -header.navbar-gitlab-new { - color: $white-light; - border-bottom: 0; - min-height: $new-navbar-height; - - .logo-text { - line-height: initial; - - svg { - width: 55px; - height: 14px; - margin: 0; - fill: $white-light; - } - } - - .header-content { - display: -webkit-flex; - display: flex; - padding-left: 0; - min-height: $new-navbar-height; - - .title-container { - display: -webkit-flex; - display: flex; - -webkit-align-items: stretch; - align-items: stretch; - -webkit-flex: 1 1 auto; - flex: 1 1 auto; - padding-top: 0; - overflow: visible; - } - - .title { - display: -webkit-flex; - display: flex; - padding-right: 0; - color: currentColor; - - img { - height: 28px; - margin-right: 8px; - } - - a { - display: -webkit-flex; - display: flex; - align-items: center; - padding: 2px 8px; - margin: 5px 2px 5px -8px; - border-radius: $border-radius-default; - - svg { - @media (min-width: $screen-sm-min) { - margin-right: 8px; - } - } - } - } - - .dropdown.open { - > a { - border-bottom-color: $white-light; - } - } - - .dropdown-menu { - margin-top: 4px; - min-width: 130px; - - @media (max-width: $screen-xs-max) { - left: auto; - right: 0; - } - } - - &.menu-expanded { - @media (max-width: $screen-xs-max) { - .title-container, - .header-logo, { - display: none; - } - } - } - } - - .dropdown-bold-header { - color: $gl-text-color-secondary; - font-size: 12px; - } - - .navbar-collapse { - padding-left: 0; - box-shadow: 0; - - @media (max-width: $screen-xs-max) { - margin-left: -8px; - margin-right: -10px; - } - - .nav { - > li:not(.hidden-xs) a { - @media (max-width: $screen-xs-max) { - margin-left: 0; - min-width: 100%; - } - } - } - } - - .container-fluid { - .navbar-toggle { - min-width: 45px; - padding: 0 $gl-padding; - margin-right: -7px; - text-align: center; - color: currentColor; - - svg { - fill: currentColor; - } - - &:hover, - &:focus, - &.active { - color: currentColor; - background-color: transparent; - - svg { - fill: currentColor; - } - } - } - - .navbar-nav { - @media (max-width: $screen-xs-max) { - display: flex; - padding-right: 10px; - } - - li { - .badge { - box-shadow: none; - font-weight: $gl-font-weight-bold; - } - } - } - - .nav > li { - &.header-user { - @media (max-width: $screen-xs-max) { - padding-left: 10px; - } - } - - > a { - will-change: color; - margin: 4px 2px; - padding: 6px 8px; - height: 32px; - - @media (max-width: $screen-xs-max) { - padding: 0; - } - - &.header-user-dropdown-toggle { - margin-left: 2px; - - .header-user-avatar { - margin-right: 0; - } - } - - &:hover, - &:focus { - text-decoration: none; - outline: 0; - opacity: 1; - color: $white-light; - - svg { - fill: currentColor; - } - - &.header-user-dropdown-toggle { - .header-user-avatar { - border-color: $white-light; - } - } - } - } - - .header-new-dropdown-toggle { - margin-right: 0; - } - - .impersonated-user, - .impersonated-user:hover { - margin-right: 1px; - background-color: $white-light; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - .impersonation-btn, - .impersonation-btn:hover { - background-color: $white-light; - margin-left: 0; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - - i { - color: $orange-500; - font-size: 20px; - } - } - - &.active > a, - &.dropdown.open > a { - - svg { - fill: currentColor; - } - } - } - } -} - -.navbar-sub-nav { - display: -webkit-flex; - display: flex; - margin: 0 0 0 6px; - - .dropdown-chevron { - position: relative; - top: -1px; - font-size: 10px; - } -} - -.navbar-gitlab-new { - .navbar-sub-nav, - .navbar-nav { - > li { - > a:hover, - > a:focus { - text-decoration: none; - outline: 0; - color: $white-light; - - svg { - fill: currentColor; - } - } - - > a { - display: flex; - align-items: center; - justify-content: center; - padding: 6px 8px; - margin: 4px 2px; - font-size: 12px; - color: currentColor; - border-radius: $border-radius-default; - height: 32px; - font-weight: $gl-font-weight-bold; - - svg { - fill: currentColor; - } - } - - &.line-separator { - margin: 8px; - } - } - } -} - -.caret-down { - height: 11px; - width: 11px; - margin-left: 4px; - fill: currentColor; -} - -.header-user .dropdown-menu-nav, -.header-new .dropdown-menu-nav { - margin-top: $dropdown-vertical-offset; -} - -.breadcrumbs { - display: flex; - min-height: 48px; - color: $gl-text-color; -} - -.breadcrumbs-container { - display: -webkit-flex; - display: flex; - width: 100%; - position: relative; - padding-top: $gl-padding / 2; - padding-bottom: $gl-padding / 2; - align-items: center; - border-bottom: 1px solid $border-color; -} - -.breadcrumbs-links { - -webkit-flex: 1; - flex: 1; - min-width: 0; - align-self: center; - color: $gl-text-color-secondary; - - .avatar-tile { - margin-right: 4px; - border: 1px solid $border-color; - border-radius: 50%; - vertical-align: sub; - } - - .text-expander { - margin-left: 0; - margin-right: 2px; - - > i { - position: relative; - top: 1px; - } - } -} - -.breadcrumbs-list { - display: -webkit-flex; - display: flex; - flex-wrap: wrap; - margin-bottom: 0; - line-height: 16px; - - > li { - display: flex; - align-items: center; - position: relative; - padding: 2px 0; - - &:not(:last-child) { - margin-right: 20px; - } - - > a { - font-size: 12px; - color: currentColor; - } - } -} - -.breadcrumb-item-text { - @include str-truncated(128px); - text-decoration: inherit; -} - -.breadcrumbs-list-angle { - position: absolute; - right: -12px; - top: 50%; - color: $gl-text-color-tertiary; - transform: translateY(-50%); -} - -.breadcrumbs-extra { - display: flex; - flex: 0 0 auto; - margin-left: auto; -} - -.breadcrumbs-sub-title { - margin: 0; - font-size: 12px; - font-weight: 600; - line-height: 16px; - - a { - color: $gl-text-color; - } -} - -.btn-sign-in { - margin-top: 3px; - font-weight: $gl-font-weight-bold; - - &:hover { - background-color: $white-light; - } -} diff --git a/app/assets/stylesheets/framework/new-sidebar.scss b/app/assets/stylesheets/framework/new-sidebar.scss index 8332cec2962..caf4c7a40b1 100644 --- a/app/assets/stylesheets/framework/new-sidebar.scss +++ b/app/assets/stylesheets/framework/new-sidebar.scss @@ -24,7 +24,7 @@ $new-sidebar-collapsed-width: 50px; // Override position: absolute .right-sidebar { position: fixed; - height: calc(100% - #{$new-navbar-height}); + height: calc(100% - #{$header-height}); } .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { @@ -87,7 +87,7 @@ $new-sidebar-collapsed-width: 50px; z-index: 400; width: $new-sidebar-width; transition: left $sidebar-transition-duration; - top: $new-navbar-height; + top: $header-height; bottom: 0; left: 0; background-color: $gray-normal; @@ -197,7 +197,7 @@ $new-sidebar-collapsed-width: 50px; } .with-performance-bar .nav-sidebar { - top: $new-navbar-height + $performance-bar-height; + top: $header-height + $performance-bar-height; } .sidebar-sub-level-items { @@ -495,7 +495,7 @@ $new-sidebar-collapsed-width: 50px; // Make issue boards full-height now that sub-nav is gone .boards-list { - height: calc(100vh - #{$new-navbar-height}); + height: calc(100vh - #{$header-height}); @media (min-width: $screen-sm-min) { height: 475px; // Needed for PhantomJS @@ -506,5 +506,5 @@ $new-sidebar-collapsed-width: 50px; } .with-performance-bar .boards-list { - height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height}); + height: calc(100vh - #{$header-height} - #{$performance-bar-height}); } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss index f8777d1fd9d..5c96b3b78e7 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss @@ -1,12 +1,11 @@ - - +// For tabbed navigation links, scrolling tabs, etc. For all top/main navigation, +// please check nav.scss .nav-links { display: flex; padding: 0; margin: 0; list-style: none; height: auto; - border-bottom: 1px solid $border-color; li { display: flex; @@ -24,7 +23,6 @@ &:active, &:focus { text-decoration: none; - border-bottom: 2px solid $gray-darkest; color: $black; .badge { @@ -34,7 +32,6 @@ } &.active a { - border-bottom: 2px solid $link-underline-blue; color: $black; font-weight: $gl-font-weight-bold; @@ -43,35 +40,6 @@ } } } - - &.sub-nav { - text-align: center; - background-color: $gray-normal; - - .container-fluid { - background-color: $gray-normal; - margin-bottom: 0; - display: flex; - } - - li { - &.active a { - border-bottom: none; - color: $link-underline-blue; - } - - a { - margin: 0; - padding: 11px 10px 9px; - - &:hover, - &:active, - &:focus { - border-color: transparent; - } - } - } - } } .top-area { @@ -91,17 +59,6 @@ } } - .nav-search { - display: inline-block; - width: 100%; - padding: 11px 0; - - /* Small devices (phones, tablets, 768px and lower) */ - @media (min-width: $screen-sm-min) { - width: 50%; - } - } - .nav-links { margin-bottom: 0; border-bottom: none; @@ -150,12 +107,6 @@ } } - &.nav-controls-new-nav { - > .dropdown { - margin-right: 0; - } - } - > .btn-grouped { float: none; } @@ -248,114 +199,43 @@ pre { width: 100%; } -} - -.project-item-select-holder.btn-group { - display: flex; - max-width: 350px; - overflow: hidden; - float: right; - - .new-project-item-link { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .new-project-item-select-button { - width: 32px; - } -} - -.empty-state .project-item-select-holder.btn-group { - float: none; - display: inline-block; - - .btn { - // overrides styles applied to plain `.empty-state .btn` - margin: 10px 0; - max-width: 300px; - width: auto; - - @media(max-width: $screen-xs-max) { - max-width: 250px; - } - - } -} -.new-project-item-select-button .fa-caret-down { - margin-left: 2px; -} - -.layout-nav { - width: 100%; - background: $gray-light; - border-bottom: 1px solid $border-color; - transition: padding $sidebar-transition-duration; - text-align: center; - margin-top: $new-navbar-height; + @media (max-width: $screen-xs-max) { + flex-flow: row wrap; - .container-fluid { - position: relative; + .nav-controls { + $controls-margin: $btn-xs-side-margin - 2px; + flex: 0 0 100%; - .nav-control { - @media (max-width: $screen-sm-max) { - margin-right: 2px; + &.controls-flex { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; + padding: 0 0 $gl-padding-top; } - } - } - - .controls { - float: right; - padding: 7px 0 0; - - i { - color: $layout-link-gray; - } - - .fa-rss, - .fa-cog { - font-size: 16px; - } - .fa-caret-down { - margin-left: 5px; - color: $gl-text-color-secondary; - } - - .dropdown { - position: absolute; - top: 7px; - right: 15px; - z-index: 300; + .controls-item, + .controls-item-full, + .controls-item:last-child { + flex: 1 1 35%; + display: block; + width: 100%; + margin: $controls-margin; - li.active { - font-weight: $gl-font-weight-bold; + .btn, + .dropdown { + margin: 0; + } } - } - } - .nav-links { - border-bottom: none; - height: 51px; - - @media (min-width: $screen-sm-min) { - justify-content: center; - } - - li { - a { - padding-top: 10px; + .controls-item-full { + flex: 1 1 100%; } } } } -.with-performance-bar .layout-nav { - margin-top: $header-height + $performance-bar-height; -} - .scrolling-tabs-container { position: relative; @@ -385,25 +265,41 @@ left: -7px; } } +} - &.sub-nav-scroll { +.inner-page-scroll-tabs { + position: relative; - .fade-right { - @include fade(left, $gray-normal); - right: 0; + .fade-right { + @include fade(left, $white-light); + right: 0; + text-align: right; - .fa { - right: -23px; - } + .fa { + right: 5px; } + } - .fade-left { - @include fade(right, $gray-normal); - left: 0; + .fade-left { + @include fade(right, $white-light); + left: 0; + text-align: left; - .fa { - left: 10px; - } + .fa { + left: 5px; + } + } + + .fade-right, + .fade-left { + top: 16px; + bottom: auto; + } + + &.is-smaller { + .fade-right, + .fade-left { + top: 11px; } } } @@ -432,41 +328,7 @@ } } } -} - -.page-with-layout-nav { - .right-sidebar { - top: ($header-height + 1) * 2; - } - - &.page-with-sub-nav { - .right-sidebar { - top: ($header-height + 1) * 3; - - &.affix { - top: $header-height; - } - } - } -} - -.with-performance-bar .page-with-layout-nav { - .right-sidebar { - top: ($header-height + 1) * 2 + $performance-bar-height; - } - - &.page-with-sub-nav { - .right-sidebar { - top: ($header-height + 1) * 3 + $performance-bar-height; - - &.affix { - top: $header-height + $performance-bar-height; - } - } - } -} -.nav-block { &.activities { border-bottom: 1px solid $border-color; @@ -476,76 +338,39 @@ } } -@media (max-width: $screen-xs-max) { - .top-area { - flex-flow: row wrap; - - .nav-controls { - $controls-margin: $btn-xs-side-margin - 2px; - flex: 0 0 100%; - - &.controls-flex { - display: flex; - flex-flow: row wrap; - align-items: center; - justify-content: center; - padding: 0 0 $gl-padding-top; - } - - .controls-item, - .controls-item-full, - .controls-item:last-child { - flex: 1 1 35%; - display: block; - width: 100%; - margin: $controls-margin; +.project-item-select-holder.btn-group { + display: flex; + max-width: 350px; + overflow: hidden; + float: right; - .btn, - .dropdown { - margin: 0; - } - } + .new-project-item-link { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } - .controls-item-full { - flex: 1 1 100%; - } - } + .new-project-item-select-button { + width: 32px; } } -.inner-page-scroll-tabs { - position: relative; - - .fade-right { - @include fade(left, $white-light); - right: 0; - text-align: right; - - .fa { - right: 5px; - } - } +.empty-state .project-item-select-holder.btn-group { + float: none; + display: inline-block; - .fade-left { - @include fade(right, $white-light); - left: 0; - text-align: left; + .btn { + // overrides styles applied to plain `.empty-state .btn` + margin: 10px 0; + max-width: 300px; + width: auto; - .fa { - left: 5px; + @media(max-width: $screen-xs-max) { + max-width: 250px; } } +} - .fade-right, - .fade-left { - top: 16px; - bottom: auto; - } - - &.is-smaller { - .fade-right, - .fade-left { - top: 11px; - } - } +.new-project-item-select-button .fa-caret-down { + margin-left: 2px; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 48dc25d343b..ef58382ba41 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -78,16 +78,16 @@ .right-sidebar { border-left: 1px solid $border-color; - height: calc(100% - #{$new-navbar-height}); + height: calc(100% - #{$header-height}); &.affix { position: fixed; - top: $new-navbar-height; + top: $header-height; } } .with-performance-bar .right-sidebar.affix { - top: $new-navbar-height + $performance-bar-height; + top: $header-height + $performance-bar-height; } @mixin maintain-sidebar-dimensions { diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 3d68a50f91f..f718ec4bcad 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -17,15 +17,19 @@ .diff-file { border: 1px solid $border-color; - border-bottom: none; margin: 0; } + + &.text-file .diff-file { + border-bottom: none; + } } .timeline-entry { border-color: $white-normal; color: $gl-text-color; border-bottom: 1px solid $border-white-light; + background: $white-light; .timeline-entry-inner { position: relative; diff --git a/app/assets/stylesheets/framework/tooltips.scss b/app/assets/stylesheets/framework/tooltips.scss new file mode 100644 index 00000000000..93baf73cb78 --- /dev/null +++ b/app/assets/stylesheets/framework/tooltips.scss @@ -0,0 +1,7 @@ +.tooltip-inner { + font-size: $tooltip-font-size; + border-radius: $border-radius-default; + line-height: 16px; + font-weight: $gl-font-weight-normal; + padding: $gl-btn-padding; +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 60260355765..bbbd16322eb 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -1,6 +1,7 @@ /* * Layout */ +$grid-size: 8px; $gutter_collapsed_width: 62px; $gutter_width: 290px; $gutter_inner_width: 250px; @@ -203,6 +204,11 @@ $code_font_size: 12px; $code_line_height: 1.6; /* + * Tooltips + */ +$tooltip-font-size: 12px; + +/* * Padding */ $gl-padding: 16px; @@ -219,8 +225,7 @@ $gl-sidebar-padding: 22px; $row-hover: $blue-50; $row-hover-border: $blue-200; $progress-color: #c0392b; -$header-height: 50px; -$new-navbar-height: 40px; +$header-height: 40px; $fixed-layout-width: 1280px; $limited-layout-width: 990px; $limited-layout-width-sm: 790px; @@ -317,6 +322,7 @@ $diff-image-info-color: grey; $diff-swipe-border: #999; $diff-view-modes-color: grey; $diff-view-modes-border: #c1c1c1; +$diff-jagged-border-gradient-color: darken($white-normal, 8%); /* * Fonts @@ -700,3 +706,15 @@ Project Templates Icons $rails: #c00; $node: #353535; $java: #70ad51; + +/* +Issuable warning +*/ +$issuable-warning-size: 24px; +$issuable-warning-icon-margin: 4px; + +/* +Image Commenting cursor +*/ +$image-comment-cursor-left-offset: 12; +$image-comment-cursor-top-offset: 30; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 3305a482a0d..ca61f7a30c3 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -414,7 +414,6 @@ margin: 5px; } -.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar, .page-with-new-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 359dd388d05..50ec5110bf1 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -64,10 +64,10 @@ color: $gl-text-color; position: sticky; position: -webkit-sticky; - top: $new-navbar-height; + top: $header-height; &.affix { - top: $new-navbar-height; + top: $header-height; } // with sidebar @@ -174,10 +174,10 @@ .with-performance-bar .build-page { .top-bar { - top: $new-navbar-height + $performance-bar-height; + top: $header-height + $performance-bar-height; &.affix { - top: $new-navbar-height + $performance-bar-height; + top: $header-height + $performance-bar-height; } } } diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss new file mode 100644 index 00000000000..5538e46a6c4 --- /dev/null +++ b/app/assets/stylesheets/pages/clusters.scss @@ -0,0 +1,9 @@ +.edit-cluster-form { + .clipboard-addon { + background-color: $white-light; + } + + .alert-block { + margin-bottom: 20px; + } +} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 994707422bb..ee3ca246374 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -54,12 +54,15 @@ .mr-widget-pipeline-graph { display: inline-block; vertical-align: middle; - margin-right: 4px; .stage-cell .stage-container { margin: 3px 3px 3px 0; } + .stage-container:last-child { + margin-right: 0; + } + .dropdown-menu { margin-top: 11px; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index fb23343b966..ffb5fc94475 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -297,6 +297,7 @@ .drag-track { display: block; position: absolute; + top: 0; left: 12px; height: 10px; width: 276px; @@ -547,16 +548,23 @@ } .diff-notes-collapse { - width: 19px; - height: 19px; + width: 24px; + height: 24px; + border-radius: 50%; padding: 0; transition: transform .1s ease-out; z-index: 100; + .collapse-icon { + height: 50%; + width: 100%; + } + svg { - vertical-align: text-top; + vertical-align: middle; } + .collapse-icon, path { fill: $white-light; } @@ -644,3 +652,157 @@ text-overflow: ellipsis; white-space: nowrap; } + +.note-container { + background-color: $gray-light; + border-top: 1px solid $white-normal; + + // double jagged line divider + .discussion-notes + .discussion-notes::before, + .discussion-notes + .discussion-form::before { + content: ''; + position: relative; + display: block; + width: 100%; + height: 10px; + background-color: $white-light; + background-image: linear-gradient(45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%), + linear-gradient(225deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%), + linear-gradient(135deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%), + linear-gradient(-45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%); + background-position: 5px 5px,0 5px,0 5px,5px 5px; + background-size: 10px 10px; + background-repeat: repeat; + } + + .notes { + position: relative; + } + + .diff-notes-collapse { + position: absolute; + left: -12px; + } +} + +.diff-file .note-container > .new-note, +.note-container .discussion-notes { + margin-left: 100px; + border-left: 1px solid $white-normal; +} + +.notes.active { + .diff-file .note-container > .new-note, + .note-container .discussion-notes { + // Override our margin and border (set for diff tab) + // when user is on the discussion tab for MR + margin-left: inherit; + border-left: inherit; + } +} + +.files:not([data-can-create-note]) .frame { + cursor: auto; +} + +.frame.click-to-comment { + position: relative; + cursor: 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) + $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; + + .comment-indicator { + position: absolute; + padding: 0; + width: (2px * $image-comment-cursor-left-offset); + height: (1px * $image-comment-cursor-top-offset); + // center the indicator to match the top left click region + margin-top: (-1px * $image-comment-cursor-top-offset) + 2; + margin-left: (-1px * $image-comment-cursor-left-offset) + 1; + + svg { + width: 100%; + height: 100%; + } + + &:focus { + outline: none; + } + } +} + +.frame .badge, +.image-diff-avatar-link .badge, +.notes > .badge { + position: absolute; + background-color: $blue-400; + color: $white-light; + border: $white-light 1px solid; + min-height: $gl-padding; + padding: 5px 8px; + border-radius: 12px; + + &:focus { + outline: none; + } +} + +.frame .badge, +.frame .image-comment-badge { + // Center align badges on the frame + transform: translate3d(-50%, -50%, 0); +} + +.image-comment-badge { + @include btn-comment-icon; + position: absolute; + + &.inverted { + border-color: $white-light; + } +} + +.image-diff-avatar-link { + position: relative; + + .badge, + .image-comment-badge { + top: 25px; + right: 8px; + } +} + +.notes > .badge { + display: none; + left: -13px; +} + +.discussion-notes { + min-height: 35px; + + &:first-child { + // First child does not have the jagged borders + min-height: 25px; + } + + &.collapsed { + background-color: $white-light; + + .diff-notes-collapse, + .note, + .discussion-reply-holder, { + display: none; + } + + .notes > .badge { + display: block; + } + } +} + +.discussion-body .image .frame { + position: relative; +} diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 9362d80d4e6..3b5e411e2c5 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -207,10 +207,13 @@ } .prometheus-state { - margin-top: 10px; + max-width: 430px; + margin: 10px auto; + text-align: center; - .state-button-section { - margin-top: 10px; + .state-svg { + max-width: 80vw; + margin: 0 auto; } } @@ -288,8 +291,14 @@ fill: $black; } - .tick > text { - font-size: 12px; + .tick { + > line { + stroke: $gray-darker; + } + + > text { + font-size: 12px; + } } .text-metric-title { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 7eb28354e6d..dae3ec7ac42 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -5,27 +5,29 @@ margin-right: auto; } -.is-confidential { +.issuable-warning-icon { color: $orange-600; background-color: $orange-100; border-radius: $border-radius-default; padding: 5px; - margin: 0 3px 0 -4px; + margin: 0 $btn-side-margin 0 0; + width: $issuable-warning-size; + height: $issuable-warning-size; + text-align: center; + + &:first-of-type { + margin-right: $issuable-warning-icon-margin; + } } -.is-not-confidential { +.sidebar-item-icon { border-radius: $border-radius-default; padding: 5px; margin: 0 3px 0 -4px; -} - -.confidentiality { - .is-not-confidential { - margin: auto; - } - .is-confidential { - margin: auto; + &.is-active { + color: $orange-600; + background-color: $orange-50; } } @@ -220,7 +222,7 @@ .right-sidebar { position: absolute; - top: $new-navbar-height; + top: $header-height; bottom: 0; right: 0; transition: width $right-sidebar-transition-duration; @@ -485,10 +487,10 @@ } .with-performance-bar .right-sidebar { - top: $new-navbar-height + $performance-bar-height; + top: $header-height + $performance-bar-height; .issuable-sidebar { - height: calc(100% - #{$new-navbar-height} - #{$performance-bar-height}); + height: calc(100% - #{$header-height} - #{$performance-bar-height}); } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 09a14578dd3..d9fb3b44d29 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -649,7 +649,7 @@ } .merge-request-tabs-holder { - top: $new-navbar-height; + top: $header-height; z-index: 200; background-color: $white-light; border-bottom: 1px solid $border-color; @@ -679,7 +679,7 @@ } .with-performance-bar .merge-request-tabs-holder { - top: $new-navbar-height + $performance-bar-height; + top: $header-height + $performance-bar-height; } .merge-request-tabs { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 74d9acb5490..04b132415eb 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -101,7 +101,7 @@ } } -.confidential-issue-warning { +.issuable-note-warning { color: $orange-600; background-color: $orange-100; border-radius: $border-radius-default $border-radius-default 0 0; @@ -110,37 +110,64 @@ padding: 3px 12px; margin: auto; align-items: center; + + .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; + + .icon svg { + position: relative; + top: 2px; + margin-right: $btn-xs-side-margin; + width: $gl-font-size; + height: $gl-font-size; + fill: $orange-600; + } } -.confidential-value { +.sidebar-item-value { .fa { background-color: inherit; } } -.confidential-warning-message { +.sidebar-item-warning-message { line-height: 1.5; padding: 16px; - .confidential-warning-message-actions { + .text { + color: $text-color; + } + + .sidebar-item-warning-message-actions { display: flex; - button { + .btn { flex-grow: 1; } } } -.confidential-issue-warning + .md-area { +.issuable-note-warning + .md-area { border-top-left-radius: 0; border-top-right-radius: 0; } .discussion-form { - padding: $gl-padding-top $gl-padding $gl-padding; background-color: $white-light; } +.discussion-form-container { + padding: $gl-padding-top $gl-padding $gl-padding; +} + .discussion-notes .disabled-comment { padding: 6px 0; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 46d31e41ada..96b7db3b85d 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -650,29 +650,12 @@ ul.notes { } .add-diff-note { + @include btn-comment-icon; opacity: 0; margin-top: -2px; - border-radius: 50%; - background: $white-light; - padding: 1px 5px; - font-size: 12px; - color: $blue-500; margin-left: -55px; position: absolute; z-index: 10; - width: 23px; - height: 23px; - border: 1px solid $blue-500; - - &:hover { - background: $blue-500; - border-color: $blue-600; - color: $white-light; - } - - &:active { - outline: 0; - } } .discussion-body, @@ -703,6 +686,12 @@ ul.notes { color: $note-disabled-comment-color; padding: 90px 0; + &.discussion-locked { + border: none; + background-color: $white-light; + } + + a { color: $gl-link-color; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 086dd528579..8fc7a5eec9b 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -209,9 +209,11 @@ } .stage-cell { - @media (min-width: $screen-md-min) { - min-width: 148px; - margin-right: -4px; + &.table-section { + @media (min-width: $screen-md-min) { + min-width: 148px; + margin-right: -4px; + } } .mini-pipeline-graph-dropdown-toggle svg { diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 67abe6e88ed..eab39f698c3 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -108,6 +108,15 @@ } } +.subkeys-list { + @include basic-list; + + li { + padding: 3px 0; + border: none; + } +} + .key-list-item { .key-list-item-info { @media (min-width: $screen-sm-min) { @@ -392,11 +401,11 @@ table.u2f-registrations { } } -.gpg-email-badge { +.email-badge { display: inline; margin-right: $gl-padding / 2; - .gpg-email-badge-email { + .email-badge-email { display: inline; margin-right: $gl-padding / 4; } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 89ebe3f9917..db0a04a5eb3 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -47,6 +47,7 @@ input[type="checkbox"]:hover { } .location-badge { + height: 32px; font-size: 12px; margin: -4px 4px -4px -4px; line-height: 25px; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 224eee90a3f..e2f6e511c86 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -169,6 +169,14 @@ } } + .tree-item-file-external-link { + margin-right: 4px; + + span { + text-decoration: inherit; + } + } + .tree_commit { max-width: 320px; diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index a4648b33cfa..c27f2ee3c09 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -3,9 +3,23 @@ # Automatically sets the layout and ensures an administrator is logged in class Admin::ApplicationController < ApplicationController before_action :authenticate_admin! + before_action :display_read_only_information layout 'admin' def authenticate_admin! render_404 unless current_user.admin? end + + def display_read_only_information + return unless Gitlab::Database.read_only? + + flash.now[:notice] = read_only_message + end + + private + + # Overridden in EE + def read_only_message + _('You are on a read-only GitLab instance.') + end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 676a7203c7d..156a8e2c515 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController def remove_email email = user.emails.find(params[:email_id]) - success = Emails::DestroyService.new(current_user, user: user, email: email.email).execute + success = Emails::DestroyService.new(current_user, user: user).execute(email) respond_to do |format| if success diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 771c6f3034a..967fe39256a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -85,12 +85,21 @@ class ApplicationController < ActionController::Base super payload[:remote_ip] = request.remote_ip - if current_user.present? - payload[:user_id] = current_user.id - payload[:username] = current_user.username + logged_user = auth_user + + if logged_user.present? + payload[:user_id] = logged_user.try(:id) + payload[:username] = logged_user.try(:username) end end + # Controllers such as GitHttpController may use alternative methods + # (e.g. tokens) to authenticate the user, whereas Devise sets current_user + def auth_user + return current_user if current_user.present? + return try(:authenticated_user) + end + # This filter handles both private tokens and personal access tokens def authenticate_user_from_private_token! token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 0d74078645a..737656b3dcc 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -10,7 +10,7 @@ module Boards def index issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute issues = issues.page(params[:page]).per(params[:per] || 20) - make_sure_position_is_set(issues) + make_sure_position_is_set(issues) if Gitlab::Database.read_write? issues = issues.preload(:project, :milestone, :assignees, diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 915f32b4c33..1126f706393 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -96,7 +96,8 @@ module NotesActions id: note.id, discussion_id: note.discussion_id(noteable), html: note_html(note), - note: note.note + note: note.note, + on_image: note.try(:on_image?) ) discussion = note.to_discussion(noteable) @@ -122,7 +123,9 @@ module NotesActions def diff_discussion_html(discussion) return unless discussion.diff_discussion? - if params[:view] == 'parallel' + on_image = discussion.on_image? + + if params[:view] == 'parallel' && !on_image template = "discussions/_parallel_diff_discussion" locals = if params[:line_type] == 'old' @@ -132,7 +135,9 @@ module NotesActions end else template = "discussions/_diff_discussion" - locals = { discussions: [discussion] } + @fresh_discussion = true + + locals = { discussions: [discussion], on_image: on_image } end render_to_string( diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 0c2646d7bf0..80ab681ed87 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -10,13 +10,14 @@ class ConfirmationsController < Devise::ConfirmationsController users_almost_there_path end - def after_confirmation_path_for(resource_name, resource) - if signed_in?(resource_name) + 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) else Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}") flash[:notice] += " Please sign in." - new_session_path(resource_name) + new_session_path(:user) end end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index a8b2b93b458..02c5857eea7 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -7,9 +7,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController def index @sort = params[:sort] @todos = @todos.page(params[:page]) - if @todos.out_of_range? && @todos.total_pages != 0 - redirect_to url_for(params.merge(page: @todos.total_pages, only_path: true)) - end + + return if redirect_out_of_range(@todos) end def destroy @@ -60,7 +59,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController end def find_todos - @todos ||= TodosFinder.new(current_user, params).execute + @todos ||= TodosFinder.new(current_user, todo_params).execute end def todos_counts @@ -69,4 +68,27 @@ class Dashboard::TodosController < Dashboard::ApplicationController done_count: number_with_delimiter(current_user.todos_done_count) } end + + def todo_params + params.permit(:action_id, :author_id, :project_id, :type, :sort, :state) + end + + def redirect_out_of_range(todos) + total_pages = + if todo_params.except(:sort, :page).empty? + (current_user.todos_pending_count / todos.limit_value).ceil + else + todos.total_pages + end + + return false if total_pages.zero? + + out_of_range = todos.current_page > total_pages + + if out_of_range + redirect_to url_for(params.merge(page: total_pages, only_path: true)) + end + + out_of_range + end end diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb new file mode 100644 index 00000000000..5551057ff55 --- /dev/null +++ b/app/controllers/google_api/authorizations_controller.rb @@ -0,0 +1,29 @@ +module GoogleApi + class AuthorizationsController < ApplicationController + def callback + token, expires_at = GoogleApi::CloudPlatform::Client + .new(nil, callback_google_api_auth_url) + .get_token(params[:code]) + + session[GoogleApi::CloudPlatform::Client.session_key_for_token] = token + session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = + expires_at.to_s + + state_redirect_uri = redirect_uri_from_session_key(params[:state]) + + if state_redirect_uri + redirect_to state_redirect_uri + else + redirect_to root_path + end + end + + private + + def redirect_uri_from_session_key(state) + key = GoogleApi::CloudPlatform::Client + .session_key_for_redirect_uri(params[:state]) + session[key] if key + end + end +end diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index 97db84b92d4..bbd7ba49d77 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -1,15 +1,14 @@ class Profiles::EmailsController < Profiles::ApplicationController + before_action :find_email, only: [:destroy, :resend_confirmation_instructions] + def index - @primary = current_user.email + @primary_email = current_user.email @emails = current_user.emails.order_id_desc end def create @email = Emails::CreateService.new(current_user, email_params.merge(user: current_user)).execute - - if @email.errors.blank? - NotificationService.new.new_email(@email) - else + unless @email.errors.blank? flash[:alert] = @email.errors.full_messages.first end @@ -17,9 +16,7 @@ class Profiles::EmailsController < Profiles::ApplicationController end def destroy - @email = current_user.emails.find(params[:id]) - - Emails::DestroyService.new(current_user, user: current_user, email: @email.email).execute + Emails::DestroyService.new(current_user, user: current_user).execute(@email) respond_to do |format| format.html { redirect_to profile_emails_url, status: 302 } @@ -27,9 +24,23 @@ class Profiles::EmailsController < Profiles::ApplicationController end end + def resend_confirmation_instructions + if Emails::ConfirmService.new(current_user, user: current_user).execute(@email) + flash[:notice] = "Confirmation email sent to #{@email.email}" + else + flash[:alert] = "There was a problem sending the confirmation email" + end + + redirect_to profile_emails_url + end + private def email_params params.require(:email).permit(:email) end + + def find_email + @email = current_user.emails.find(params[:id]) + end end diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb index 689c76059f6..38e3eacd229 100644 --- a/app/controllers/profiles/gpg_keys_controller.rb +++ b/app/controllers/profiles/gpg_keys_controller.rb @@ -2,7 +2,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController before_action :set_gpg_key, only: [:destroy, :revoke] def index - @gpg_keys = current_user.gpg_keys + @gpg_keys = current_user.gpg_keys.with_subkeys @gpg_key = GpgKey.new end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index eb010923466..0837451cc49 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -29,13 +29,17 @@ class Projects::ArtifactsController < Projects::ApplicationController blob = @entry.blob conditionally_expand_blob(blob) - respond_to do |format| - format.html do - render 'file' - end - - format.json do - render_blob_json(blob) + if blob.external_link?(build) + redirect_to blob.external_url(@project, build) + else + respond_to do |format| + format.html do + render 'file' + end + + format.json do + render_blob_json(blob) + end end end end diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb new file mode 100644 index 00000000000..03019b0becc --- /dev/null +++ b/app/controllers/projects/clusters_controller.rb @@ -0,0 +1,136 @@ +class Projects::ClustersController < Projects::ApplicationController + before_action :cluster, except: [:login, :index, :new, :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_update_cluster!, only: [:update] + before_action :authorize_admin_cluster!, only: [:destroy] + + def index + if project.cluster + redirect_to project_cluster_path(project, project.cluster) + else + redirect_to new_project_cluster_path(project) + end + end + + def login + begin + state = generate_session_key_redirect(namespace_project_clusters_url.to_s) + + @authorize_url = GoogleApi::CloudPlatform::Client.new( + nil, callback_google_api_auth_url, + state: state).authorize_url + rescue GoogleApi::Auth::ConfigMissingError + # no-op + end + end + + def new + @cluster = project.build_cluster + end + + def create + @cluster = Ci::CreateClusterService + .new(project, current_user, create_params) + .execute(token_in_session) + + if @cluster.persisted? + redirect_to project_cluster_path(project, @cluster) + else + render :new + end + end + + def status + respond_to do |format| + format.json do + Gitlab::PollingInterval.set_header(response, interval: 10_000) + + render json: ClusterSerializer + .new(project: @project, current_user: @current_user) + .represent_status(@cluster) + end + end + end + + def show + end + + def update + Ci::UpdateClusterService + .new(project, current_user, update_params) + .execute(cluster) + + if cluster.valid? + flash[:notice] = "Cluster was successfully updated." + redirect_to project_cluster_path(project, project.cluster) + else + render :show + end + end + + def destroy + if cluster.destroy + flash[:notice] = "Cluster integration was successfully removed." + redirect_to project_clusters_path(project), status: 302 + else + flash[:notice] = "Cluster integration was not removed." + render :show + end + end + + private + + def cluster + @cluster ||= project.cluster.present(current_user: current_user) + end + + 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) + end + + def update_params + params.require(:cluster).permit( + :project_namespace, + :enabled) + end + + def authorize_google_api + unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil) + .validate_token(expires_at_in_session) + redirect_to action: 'login' + end + end + + def token_in_session + @token_in_session ||= + session[GoogleApi::CloudPlatform::Client.session_key_for_token] + end + + def expires_at_in_session + @expires_at_in_session ||= + session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] + end + + def generate_session_key_redirect(uri) + GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key| + session[key] = uri + end + end + + def authorize_update_cluster! + access_denied! unless can?(current_user, :update_cluster, cluster) + end + + def authorize_admin_cluster! + access_denied! unless can?(current_user, :admin_cluster, cluster) + end +end diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 7d0e2b3e2ef..95d7a02e9e9 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -9,6 +9,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true alias_method :user, :actor + alias_method :authenticated_user, :actor # Git clients will not know what authenticity token to send along skip_before_action :verify_authenticity_token diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index ee6e6f80cdd..b7a108a0ebd 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -278,6 +278,7 @@ class Projects::IssuesController < Projects::ApplicationController state_event task_num lock_version + discussion_locked ] + [{ label_ids: [], assignee_ids: [] }] end diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index 96abdac91b6..1b985ea9763 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -11,7 +11,7 @@ class Projects::JobsController < Projects::ApplicationController def index @scope = params[:scope] @all_builds = project.builds.relevant - @builds = @all_builds.order('created_at DESC') + @builds = @all_builds.order('ci_builds.id DESC') @builds = case @scope when 'pending' diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index 1b0d3aab3fa..536f908d2c5 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -2,6 +2,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController include LfsRequest skip_before_action :lfs_check_access!, only: [:deprecated] + before_action :lfs_check_batch_operation!, only: [:batch] def batch unless objects.present? @@ -90,4 +91,21 @@ class Projects::LfsApiController < Projects::GitHttpClientController } } end + + def lfs_check_batch_operation! + if upload_request? && Gitlab::Database.read_only? + render( + json: { + message: lfs_read_only_message + }, + content_type: 'application/vnd.git-lfs+json', + status: 403 + ) + end + end + + # Overridden in EE + def lfs_read_only_message + _('You cannot write to this read-only GitLab instance.') + end end diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 6602b204fcb..0e71977a58a 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -13,7 +13,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont # Make sure merge requests created before 8.0 # have head file in refs/merge-requests/ def ensure_ref_fetched - @merge_request.ensure_ref_fetched + @merge_request.ensure_ref_fetched if Gitlab::Database.read_write? end def merge_request_params @@ -34,6 +34,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont :target_project_id, :task_num, :title, + :discussion_locked, label_ids: [] ] diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 1096afbb798..99dc3dda9e7 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -120,10 +120,13 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap end def selected_target_project - if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil? + if @project.id.to_s == params[:target_project_id] || !@project.forked? @project + elsif params[:target_project_id].present? + MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project) + .execute.find(params[:target_project_id]) else - @project.forked_project_link.forked_from_project + @project.forked_from_project end end end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 41a13f6f577..ef7d047b1ad 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -66,7 +66,16 @@ class Projects::NotesController < Projects::ApplicationController params.merge(last_fetched_at: last_fetched_at) end + def authorize_admin_note! + return access_denied! unless can?(current_user, :admin_note, note) + end + def authorize_resolve_note! return access_denied! unless can?(current_user, :resolve_note, note) end + + def authorize_create_note! + return unless noteable.lockable? + access_denied! unless can?(current_user, :create_note, noteable) + end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 5ea3a5d5562..d9142311b6f 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -25,18 +25,33 @@ class RegistrationsController < Devise::RegistrationsController end def destroy - current_user.delete_async(deleted_by: current_user) - - respond_to do |format| - format.html do - session.try(:destroy) - redirect_to new_user_session_path, status: 302, notice: "Account scheduled for removal." - end + if destroy_confirmation_valid? + current_user.delete_async(deleted_by: current_user) + session.try(:destroy) + redirect_to new_user_session_path, status: 303, notice: s_('Profiles|Account scheduled for removal.') + else + redirect_to profile_account_path, status: 303, alert: destroy_confirmation_failure_message end end protected + def destroy_confirmation_valid? + if current_user.confirm_deletion_with_password? + current_user.valid_password?(params[:password]) + else + current_user.username == params[:username] + end + end + + def destroy_confirmation_failure_message + if current_user.confirm_deletion_with_password? + s_('Profiles|Invalid password') + else + s_('Profiles|Invalid username') + end + end + def build_resource(hash = nil) super end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 4223c6171a6..c01be42c3ee 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -8,12 +8,11 @@ class SessionsController < Devise::SessionsController prepend_before_action :check_initial_setup, only: [:new] prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] - prepend_before_action :store_redirect_path, only: [:new] - + prepend_before_action :store_redirect_uri, only: [:new] before_action :auto_sign_in_with_provider, only: [:new] before_action :load_recaptcha - after_action :log_failed_login, only: [:new] + after_action :log_failed_login, only: [:new], if: :failed_login? def new set_minimum_password_length @@ -46,8 +45,6 @@ class SessionsController < Devise::SessionsController private def log_failed_login - return unless failed_login? - Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}") end @@ -88,28 +85,36 @@ class SessionsController < Devise::SessionsController end end - def store_redirect_path - redirect_path = + def stored_redirect_uri + @redirect_to ||= stored_location_for(:redirect) + end + + def store_redirect_uri + redirect_uri = if request.referer.present? && (params['redirect_to_referer'] == 'yes') - referer_uri = URI(request.referer) - if referer_uri.host == Gitlab.config.gitlab.host - referer_uri.request_uri - else - request.fullpath - end + URI(request.referer) else - request.fullpath + URI(request.url) end # Prevent a 'you are already signed in' message directly after signing: # we should never redirect to '/users/sign_in' after signing in successfully. - unless URI(redirect_path).path == new_user_session_path - store_location_for(:redirect, redirect_path) - end + return true if redirect_uri.path == new_user_session_path + + redirect_to = redirect_uri.to_s if redirect_allowed_to?(redirect_uri) + + @redirect_to = redirect_to + store_location_for(:redirect, redirect_to) + end + + # Overridden in EE + def redirect_allowed_to?(uri) + uri.host == Gitlab.config.gitlab.host && + uri.port == Gitlab.config.gitlab.port end def two_factor_enabled? - find_user.try(:two_factor_enabled?) + find_user&.two_factor_enabled? end def auto_sign_in_with_provider diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb new file mode 100644 index 00000000000..189eb3847eb --- /dev/null +++ b/app/finders/merge_request_target_project_finder.rb @@ -0,0 +1,18 @@ +class MergeRequestTargetProjectFinder + attr_reader :current_user, :source_project + + def initialize(current_user: nil, source_project:) + @current_user = current_user + @source_project = source_project + end + + def execute + if @source_project.fork_network + @source_project.fork_network.projects + .public_or_visible_to_user(current_user) + .with_feature_available_for_user(:merge_requests, current_user) + else + Project.where(id: source_project) + end + end +end 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/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/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/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index c31023f2d9a..5b2c58d193d 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -73,7 +73,8 @@ module MergeRequestsHelper end def target_projects(project) - [project, project.default_merge_request_target].uniq + MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: project) + .execute end def merge_request_button_visibility(merge_request, closed) diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index ce028195e51..c219aa3d6a9 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -130,8 +130,12 @@ module NotesHelper end def can_create_note? + issuable = @issue || @merge_request + if @snippet.is_a?(PersonalSnippet) can?(current_user, :comment_personal_snippet, @snippet) + elsif issuable + can?(current_user, :create_note, issuable) else can?(current_user, :create_note, @project) end diff --git a/app/helpers/numbers_helper.rb b/app/helpers/numbers_helper.rb new file mode 100644 index 00000000000..45bd3606076 --- /dev/null +++ b/app/helpers/numbers_helper.rb @@ -0,0 +1,11 @@ +module NumbersHelper + def limited_counter_with_delimiter(resource, **options) + limit = options.fetch(:limit, 1000).to_i + count = resource.limit(limit + 1).count(:all) + if count > limit + number_with_delimiter(count - 1, options) + '+' + else + number_with_delimiter(count, options) + end + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 4c0cce54527..20e050195ea 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -293,6 +293,7 @@ module ProjectsHelper snippets: :read_project_snippet, settings: :admin_project, builds: :read_build, + clusters: :read_cluster, labels: :read_label, issues: :read_issue, project_members: :read_project_member, diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb index d7eaf6ce24d..00fe67d6ffb 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -19,7 +19,9 @@ module SystemNoteHelper 'discussion' => 'comment', 'moved' => 'arrow-right', 'outdated' => 'pencil', - 'duplicate' => 'issue-duplicate' + 'duplicate' => 'issue-duplicate', + 'locked' => 'lock', + 'unlocked' => 'lock-open' }.freeze def system_note_icon_name(note) diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index c401030e34a..4f5edeb9bda 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -7,12 +7,6 @@ module Emails mail(to: @user.notification_email, subject: subject("Account was created for you")) end - def new_email_email(email_id) - @email = Email.find(email_id) - @current_user = @user = @email.user - mail(to: @user.notification_email, subject: subject("Email was added to your account")) - end - def new_ssh_key_email(key_id) @key = Key.find_by(id: key_id) diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb index b35febc9ac5..8b66531ec7b 100644 --- a/app/models/ci/artifact_blob.rb +++ b/app/models/ci/artifact_blob.rb @@ -2,6 +2,8 @@ module Ci class ArtifactBlob include BlobLike + EXTENTIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json].freeze + attr_reader :entry def initialize(entry) @@ -17,6 +19,7 @@ module Ci def size entry.metadata[:size] end + alias_method :external_size, :size def data "Build artifact #{path}" @@ -30,6 +33,27 @@ module Ci :build_artifact end - alias_method :external_size, :size + 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('/') + + "#{pages_config.protocol}://#{components[0]}.#{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)) && + job.project.public? + end + + private + + def pages_config + Gitlab.config.pages + end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index dd315866e60..6ca46ae89c1 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -11,6 +11,7 @@ module Ci has_many :deployments, as: :deployable has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment' + has_many :trace_sections, class_name: 'Ci::BuildTraceSection' # The "environment" field for builds is a String, and is the unexpanded name def persisted_environment @@ -265,6 +266,10 @@ module Ci update_attributes(coverage: coverage) if coverage.present? end + def parse_trace_sections! + ExtractSectionsFromBuildTraceService.new(project, user).execute(self) + end + def trace Gitlab::Ci::Trace.new(self) end diff --git a/app/models/ci/build_trace_section.rb b/app/models/ci/build_trace_section.rb new file mode 100644 index 00000000000..ccdb95546c8 --- /dev/null +++ b/app/models/ci/build_trace_section.rb @@ -0,0 +1,11 @@ +module Ci + class BuildTraceSection < ActiveRecord::Base + extend Gitlab::Ci::Model + + belongs_to :build, class_name: 'Ci::Build' + belongs_to :project + belongs_to :section_name, class_name: 'Ci::BuildTraceSectionName' + + validates :section_name, :build, :project, presence: true, allow_blank: false + end +end diff --git a/app/models/ci/build_trace_section_name.rb b/app/models/ci/build_trace_section_name.rb new file mode 100644 index 00000000000..0fdcb1ea329 --- /dev/null +++ b/app/models/ci/build_trace_section_name.rb @@ -0,0 +1,11 @@ +module Ci + class BuildTraceSectionName < ActiveRecord::Base + extend Gitlab::Ci::Model + + belongs_to :project + has_many :trace_sections, class_name: 'Ci::BuildTraceSection', foreign_key: :section_name_id + + validates :name, :project, presence: true, allow_blank: false + validates :name, uniqueness: { scope: :project_id } + end +end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 3d5acc00f8f..cf3ce3c9e54 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -5,6 +5,7 @@ module Ci include Importable include AfterCommitQueue include Presentable + include Gitlab::OptimisticLocking belongs_to :project belongs_to :user @@ -58,6 +59,11 @@ module Ci auto_devops_source: 2 } + enum failure_reason: { + unknown_failure: 0, + config_error: 1 + } + state_machine :status, initial: :created do event :enqueue do transition created: :pending @@ -109,6 +115,12 @@ module Ci pipeline.auto_canceled_by = nil end + before_transition any => :failed do |pipeline, transition| + transition.args.first.try do |reason| + pipeline.failure_reason = reason + end + end + after_transition [:created, :pending] => :running do |pipeline| pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) } end @@ -263,7 +275,7 @@ module Ci end def cancel_running - Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable| + retry_optimistic_lock(cancelable_statuses) do |cancelable| cancelable.find_each do |job| yield(job) if block_given? job.cancel @@ -312,6 +324,10 @@ module Ci @stage_seeds ||= config_processor.stage_seeds(self) end + def seeds_size + @seeds_size ||= stage_seeds.sum(&:size) + end + def has_kubernetes_active? project.kubernetes_service&.active? end @@ -403,7 +419,7 @@ module Ci end def update_status - Gitlab::OptimisticLocking.retry_lock(self) do + retry_optimistic_lock(self) do case latest_builds_status when 'pending' then enqueue when 'running' then run 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 193e459977a..9417033d1f6 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -59,7 +59,7 @@ module CacheMarkdownField # Update every column in a row if any one is invalidated, as we only store # one version per row - def refresh_markdown_cache!(do_update: false) + def refresh_markdown_cache options = { skip_project_check: skip_project_check? } updates = cached_markdown_fields.markdown_fields.map do |markdown_field| @@ -71,8 +71,14 @@ module CacheMarkdownField updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION updates.each {|html_field, data| write_attribute(html_field, data) } + end + + def refresh_markdown_cache! + updates = refresh_markdown_cache + + return unless persisted? && Gitlab::Database.read_write? - update_columns(updates) if persisted? && do_update + update_columns(updates) end def cached_html_up_to_date?(markdown_field) @@ -124,8 +130,8 @@ module CacheMarkdownField end # Using before_update here conflicts with elasticsearch-model somehow - before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache? - before_update :refresh_markdown_cache!, if: :invalidated_markdown_cache? + before_create :refresh_markdown_cache, if: :invalidated_markdown_cache? + before_update :refresh_markdown_cache, if: :invalidated_markdown_cache? end class_methods do diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb index eee1a36ac6b..f5cbb3becad 100644 --- a/app/models/concerns/discussion_on_diff.rb +++ b/app/models/concerns/discussion_on_diff.rb @@ -28,6 +28,10 @@ module DiscussionOnDiff true end + def file_new_path + first_note.position.new_path + end + # Returns an array of at most 16 highlighted lines above a diff note def truncated_diff_lines(highlight: true) lines = highlight ? highlighted_diff_lines : diff_lines diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 3803e18a96e..7c3ed96bc28 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -81,6 +81,7 @@ module HasStatus scope :canceled, -> { where(status: 'canceled') } scope :skipped, -> { where(status: 'skipped') } scope :manual, -> { where(status: 'manual') } + scope :alive, -> { where(status: [:created, :pending, :running]) } scope :created_or_pending, -> { where(status: [:created, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) } diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index fc30d008dea..27f4dedffd3 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -256,23 +256,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/noteable.rb b/app/models/concerns/noteable.rb index 1c4ddabcad5..5d75b2aa6a3 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -74,4 +74,8 @@ module Noteable def discussions_can_be_resolved_by?(user) discussions_to_be_resolved.all? { |discussion| discussion.can_resolve?(user) } end + + def lockable? + [MergeRequest, Issue].include?(self.class) + end end diff --git a/app/models/concerns/repository_mirroring.rb b/app/models/concerns/repository_mirroring.rb index fed336c29d6..f6aba91bc4c 100644 --- a/app/models/concerns/repository_mirroring.rb +++ b/app/models/concerns/repository_mirroring.rb @@ -1,11 +1,26 @@ module RepositoryMirroring - def set_remote_as_mirror(name) - config = raw_repository.rugged.config + 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" - config["remote.#{name}.fetch"] = 'refs/*:refs/*' - config["remote.#{name}.mirror"] = true - config["remote.#{name}.prune"] = true + 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) diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index f5048d17d80..22fde2eb134 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -106,6 +106,10 @@ module Routable RequestStore[full_path_key] ||= uncached_full_path end + def full_path_components + full_path.split('/') + end + def expires_full_path_cache RequestStore.delete(full_path_key) if RequestStore.active? @full_path = nil @@ -152,6 +156,8 @@ module Routable end def update_route + return if Gitlab::Database.read_only? + prepare_route route.save end diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index a7d5de48c66..ec3543f7053 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -43,15 +43,17 @@ module TokenAuthenticatable write_attribute(token_field, token) if token end + # Returns a token, but only saves when the database is in read & write mode define_method("ensure_#{token_field}!") do send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend read_attribute(token_field) end + # Resets the token, but only saves when the database is in read & write mode define_method("reset_#{token_field}!") do write_new_token(token_field) - save! + save! if Gitlab::Database.read_write? end end end diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb index 07c4846e2ac..6eba87da1a1 100644 --- a/app/models/diff_discussion.rb +++ b/app/models/diff_discussion.rb @@ -11,6 +11,8 @@ class DiffDiscussion < Discussion delegate :position, :original_position, :change_position, + :on_text?, + :on_image?, to: :first_note diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index e9a60e6ce09..d88a92dc027 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -12,8 +12,8 @@ class DiffNote < Note validates :original_position, presence: true validates :position, presence: true - validates :diff_line, presence: true - validates :line_code, presence: true, line_code: true + validates :diff_line, presence: true, if: :on_text? + validates :line_code, presence: true, line_code: true, if: :on_text? validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } validate :positions_complete validate :verify_supported @@ -43,6 +43,14 @@ class DiffNote < Note end end + def on_text? + position.position_type == "text" + end + + def on_image? + position.position_type == "image" + end + def diff_file @diff_file ||= self.original_position.diff_file(self.project.repository) end @@ -56,6 +64,8 @@ class DiffNote < Note end def original_line_code + return unless on_text? + self.diff_file.line_code(self.diff_line) end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index b80da7b246a..437df923d2d 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -66,6 +66,10 @@ class Discussion @context_noteable = context_noteable end + def on_image? + false + end + def ==(other) other.class == self.class && other.context_noteable == self.context_noteable && diff --git a/app/models/email.rb b/app/models/email.rb index 826d4f16edb..384f38f2db7 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -7,6 +7,13 @@ class Email < ActiveRecord::Base validates :email, presence: true, uniqueness: true, email: true validate :unique_email, if: ->(email) { email.email_changed? } + scope :confirmed, -> { where.not(confirmed_at: nil) } + + after_commit :update_invalid_gpg_signatures, if: -> { previous_changes.key?('confirmed_at') } + + devise :confirmable + self.reconfirmable = false # currently email can't be changed, no need to reconfirm + def email=(value) write_attribute(:email, value.downcase.strip) end @@ -14,4 +21,9 @@ class Email < ActiveRecord::Base def unique_email self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email) end + + # once email is confirmed, update the gpg signatures + def update_invalid_gpg_signatures + user.update_invalid_gpg_signatures if confirmed? + end end diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb new file mode 100644 index 00000000000..218e37a5312 --- /dev/null +++ b/app/models/fork_network.rb @@ -0,0 +1,15 @@ +class ForkNetwork < ActiveRecord::Base + belongs_to :root_project, class_name: 'Project' + has_many :fork_network_members + has_many :projects, through: :fork_network_members + + after_create :add_root_as_member, if: :root_project + + def add_root_as_member + projects << root_project + end + + def find_forks_in(other_projects) + projects.where(id: other_projects) + end +end diff --git a/app/models/fork_network_member.rb b/app/models/fork_network_member.rb new file mode 100644 index 00000000000..6a9b52a1ef8 --- /dev/null +++ b/app/models/fork_network_member.rb @@ -0,0 +1,7 @@ +class ForkNetworkMember < ActiveRecord::Base + belongs_to :fork_network + belongs_to :project + belongs_to :forked_from_project, class_name: 'Project' + + validates :fork_network, :project, presence: true +end diff --git a/app/models/gcp/cluster.rb b/app/models/gcp/cluster.rb new file mode 100644 index 00000000000..18bd6a6dcb4 --- /dev/null +++ b/app/models/gcp/cluster.rb @@ -0,0 +1,113 @@ +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/gpg_key.rb b/app/models/gpg_key.rb index 54bd5b68777..44eda741679 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -9,6 +9,9 @@ class GpgKey < ActiveRecord::Base belongs_to :user has_many :gpg_signatures + has_many :subkeys, class_name: 'GpgKeySubkey' + + scope :with_subkeys, -> { includes(:subkeys) } validates :user, presence: true @@ -36,10 +39,12 @@ class GpgKey < ActiveRecord::Base before_validation :extract_fingerprint, :extract_primary_keyid after_commit :update_invalid_gpg_signatures, on: :create + after_create :generate_subkeys def primary_keyid super&.upcase end + alias_method :keyid, :primary_keyid def fingerprint super&.upcase @@ -49,6 +54,10 @@ class GpgKey < ActiveRecord::Base super(value&.strip) end + def keyids + [keyid].concat(subkeys.map(&:keyid)) + end + def user_infos @user_infos ||= Gitlab::Gpg.user_infos_from_key(key) end @@ -82,10 +91,11 @@ class GpgKey < ActiveRecord::Base def revoke GpgSignature - .where(gpg_key: self) + .with_key_and_subkeys(self) .where.not(verification_status: GpgSignature.verification_statuses[:unknown_key]) .update_all( gpg_key_id: nil, + gpg_key_subkey_id: nil, verification_status: GpgSignature.verification_statuses[:unknown_key], updated_at: Time.zone.now ) @@ -106,4 +116,12 @@ class GpgKey < ActiveRecord::Base # only allows one key self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first end + + def generate_subkeys + gpg_subkeys = Gitlab::Gpg.subkeys_from_key(key) + + gpg_subkeys[primary_keyid]&.each do |subkey_data| + subkeys.create!(keyid: subkey_data[:keyid], fingerprint: subkey_data[:fingerprint]) + end + end end diff --git a/app/models/gpg_key_subkey.rb b/app/models/gpg_key_subkey.rb new file mode 100644 index 00000000000..b57922aba30 --- /dev/null +++ b/app/models/gpg_key_subkey.rb @@ -0,0 +1,22 @@ +class GpgKeySubkey < ActiveRecord::Base + include ShaAttribute + + sha_attribute :keyid + sha_attribute :fingerprint + + belongs_to :gpg_key + + validates :gpg_key_id, presence: true + validates :fingerprint, :keyid, presence: true, uniqueness: true + + delegate :key, :user, :user_infos, :verified?, :verified_user_infos, + :verified_and_belongs_to_email?, to: :gpg_key + + def keyid + super&.upcase + end + + def fingerprint + super&.upcase + end +end diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 1f047a32c84..bf88d75246f 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -15,11 +15,42 @@ class GpgSignature < ActiveRecord::Base belongs_to :project belongs_to :gpg_key + belongs_to :gpg_key_subkey validates :commit_sha, presence: true validates :project_id, presence: true validates :gpg_key_primary_keyid, presence: true + def self.with_key_and_subkeys(gpg_key) + subkey_ids = gpg_key.subkeys.pluck(:id) + + where( + arel_table[:gpg_key_id].eq(gpg_key.id).or( + arel_table[:gpg_key_subkey_id].in(subkey_ids) + ) + ) + end + + def gpg_key=(model) + case model + when GpgKey + super + when GpgKeySubkey + self.gpg_key_subkey = model + when NilClass + super + self.gpg_key_subkey = nil + end + end + + def gpg_key + if gpg_key_id + super + elsif gpg_key_subkey_id + gpg_key_subkey + end + end + def gpg_key_primary_keyid super&.upcase end @@ -29,6 +60,8 @@ class GpgSignature < ActiveRecord::Base end def gpg_commit + return unless commit + Gitlab::Gpg::Commit.new(commit) end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 155c5d972b7..36e4108b9d6 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -74,20 +74,6 @@ 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 - def self.reference_prefix '#' end @@ -131,6 +117,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/legacy_diff_discussion.rb b/app/models/legacy_diff_discussion.rb index 3c1d34db5fa..80fc6304fd4 100644 --- a/app/models/legacy_diff_discussion.rb +++ b/app/models/legacy_diff_discussion.rb @@ -17,6 +17,14 @@ class LegacyDiffDiscussion < Discussion true end + def on_image? + false + end + + def on_text? + true + end + def active?(*args) return @active if @active.present? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0ba00d447e8..972a35dde4d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -179,6 +179,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 { @@ -403,7 +407,7 @@ class MergeRequest < ActiveRecord::Base return false unless for_fork? return true unless source_project - !source_project.forked_from?(target_project) + !source_project.in_fork_network_of?(target_project) end def reopenable? @@ -415,6 +419,8 @@ class MergeRequest < ActiveRecord::Base end def create_merge_request_diff + fetch_ref + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435 Gitlab::GitalyClient.allow_n_plus_1_calls do merge_request_diffs.create @@ -462,6 +468,7 @@ class MergeRequest < ActiveRecord::Base return unless open? old_diff_refs = self.diff_refs + create_merge_request_diff MergeRequests::MergeRequestDiffCacheService.new.execute(self) new_diff_refs = self.diff_refs @@ -474,7 +481,7 @@ class MergeRequest < ActiveRecord::Base end def check_if_can_be_merged - return unless unchecked? + return unless unchecked? && Gitlab::Database.read_write? can_be_merged = !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch) @@ -584,24 +591,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 @@ -686,13 +675,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) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 58050e1f438..faf0b95f842 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -55,7 +55,6 @@ class MergeRequestDiff < ActiveRecord::Base end def ensure_commit_shas - merge_request.fetch_ref self.start_commit_sha ||= merge_request.target_branch_sha self.head_commit_sha ||= merge_request.source_branch_sha self.base_commit_sha ||= find_base_sha diff --git a/app/models/namespace.rb b/app/models/namespace.rb index a6f517ce2ce..0601a61a926 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -139,7 +139,9 @@ class Namespace < ActiveRecord::Base end def find_fork_of(project) - projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id) + return nil unless project.fork_network + + project.fork_network.find_forks_in(projects).first end def lfs_enabled? diff --git a/app/models/note.rb b/app/models/note.rb index f44590e2144..ceded9f2aef 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -134,14 +134,22 @@ class Note < ActiveRecord::Base Discussion.build(notes) end + # Group diff discussions by line code or file path. + # It is not needed to group by line code when comment is + # on an image. def grouped_diff_discussions(diff_refs = nil) groups = {} diff_notes.fresh.discussions.each do |discussion| - line_code = discussion.line_code_in_diffs(diff_refs) - - if line_code - discussions = groups[line_code] ||= [] + group_key = + if discussion.on_image? + discussion.file_new_path + else + discussion.line_code_in_diffs(diff_refs) + end + + if group_key + discussions = groups[group_key] ||= [] discussions << discussion end end diff --git a/app/models/project.rb b/app/models/project.rb index fdbc1f5d50c..6e1cf9e31ee 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -121,11 +121,20 @@ class Project < ActiveRecord::Base has_one :mock_monitoring_service has_one :microsoft_teams_service + # TODO: replace these relations with the fork network versions has_one :forked_project_link, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link has_many :forked_project_links, foreign_key: "forked_from_project_id" has_many :forks, through: :forked_project_links, source: :forked_to_project + # TODO: replace these relations with the fork network versions + + has_one :root_of_fork_network, + foreign_key: 'root_project_id', + inverse_of: :root_project, + class_name: 'ForkNetwork' + has_one :fork_network_member + has_one :fork_network, through: :fork_network_member # Merge Requests for target project should be removed with it has_many :merge_requests, foreign_key: 'target_project_id' @@ -168,6 +177,7 @@ 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 # Container repositories need to remove data from the container registry, # which is not managed by the DB. Hence we're still using dependent: :destroy @@ -182,6 +192,7 @@ class Project < ActiveRecord::Base # bulk that doesn't involve loading the rows into memory. As a result we're # still using `dependent: :destroy` here. has_many :builds, class_name: 'Ci::Build', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName' has_many :runner_projects, class_name: 'Ci::RunnerProject' has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :variables, class_name: 'Ci::Variable' @@ -823,7 +834,7 @@ class Project < ActiveRecord::Base end def cache_has_external_issue_tracker - update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) + update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write? end def has_wiki? @@ -843,7 +854,7 @@ class Project < ActiveRecord::Base end def cache_has_external_wiki - update_column(:has_external_wiki, services.external_wikis.any?) + update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write? end def find_or_initialize_services(exceptions: []) @@ -1008,6 +1019,11 @@ class Project < ActiveRecord::Base end def forked? + return true if fork_network && fork_network.root_project != self + + # TODO: Use only the above conditional using the `fork_network` + # This is the old conditional that looks at the `forked_project_link`, we + # fall back to this while we're migrating the new models !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?) end @@ -1044,6 +1060,8 @@ class Project < ActiveRecord::Base end true + rescue GRPC::Internal # if the path is too long + false end def create_repository(force: false) @@ -1125,8 +1143,19 @@ class Project < ActiveRecord::Base end end - def forked_from?(project) - forked? && project == forked_from_project + def forked_from?(other_project) + forked? && forked_from_project == other_project + end + + def in_fork_network_of?(other_project) + # TODO: Remove this in a next release when all fork_networks are populated + # This makes sure all MergeRequests remain valid while the projects don't + # have a fork_network yet. + return true if forked_from?(other_project) + + return false if fork_network.nil? || other_project.fork_network.nil? + + fork_network == other_project.fork_network end def origin_merge_requests diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb index e2ad586aea7..22a65b5145e 100644 --- a/app/models/project_services/chat_message/base_message.rb +++ b/app/models/project_services/chat_message/base_message.rb @@ -3,6 +3,7 @@ require 'slack-notifier' module ChatMessage class BaseMessage attr_reader :markdown + attr_reader :user_full_name attr_reader :user_name attr_reader :user_avatar attr_reader :project_name @@ -12,10 +13,19 @@ module ChatMessage @markdown = params[:markdown] || false @project_name = params.dig(:project, :path_with_namespace) || params[:project_name] @project_url = params.dig(:project, :web_url) || params[:project_url] + @user_full_name = params.dig(:user, :name) || params[:user_full_name] @user_name = params.dig(:user, :username) || params[:user_name] @user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar] end + def user_combined_name + if user_full_name.present? + "#{user_full_name} (#{user_name})" + else + user_name + end + end + def pretext return message if markdown diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb index 4b9a2b1e1f3..1327b075858 100644 --- a/app/models/project_services/chat_message/issue_message.rb +++ b/app/models/project_services/chat_message/issue_message.rb @@ -29,7 +29,7 @@ module ChatMessage def activity { - title: "Issue #{state} by #{user_name}", + title: "Issue #{state} by #{user_combined_name}", subtitle: "in #{project_link}", text: issue_link, image: user_avatar @@ -40,9 +40,9 @@ module ChatMessage def message if state == 'opened' - "[#{project_link}] Issue #{state} by #{user_name}" + "[#{project_link}] Issue #{state} by #{user_combined_name}" else - "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}" + "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}" end end diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb index 7d0de81cdf0..f412b6833d9 100644 --- a/app/models/project_services/chat_message/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -24,7 +24,7 @@ module ChatMessage def activity { - title: "Merge Request #{state} by #{user_name}", + title: "Merge Request #{state} by #{user_combined_name}", subtitle: "in #{project_link}", text: merge_request_link, image: user_avatar @@ -46,7 +46,7 @@ module ChatMessage end def merge_request_message - "#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}" + "#{user_combined_name} #{state} #{merge_request_link} in #{project_link}: #{title}" end def merge_request_link diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb index 2da4c244229..7f9486132e6 100644 --- a/app/models/project_services/chat_message/note_message.rb +++ b/app/models/project_services/chat_message/note_message.rb @@ -32,7 +32,7 @@ module ChatMessage def activity { - title: "#{user_name} #{link('commented on ' + target, note_url)}", + title: "#{user_combined_name} #{link('commented on ' + target, note_url)}", subtitle: "in #{project_link}", text: formatted_title, image: user_avatar @@ -42,7 +42,7 @@ module ChatMessage private def message - "#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*" + "#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*" end def format_title(title) diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index d63d4ec2b12..2135122278a 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -9,7 +9,7 @@ module ChatMessage def initialize(data) super - @user_name = data.dig(:user, :name) || 'API' + @user_name = data.dig(:user, :username) || 'API' pipeline_attributes = data[:object_attributes] @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' @@ -35,7 +35,7 @@ module ChatMessage def activity { - title: "Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_name} #{humanized_status}", + title: "Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_combined_name} #{humanized_status}", subtitle: "in #{project_link}", text: "in #{pretty_duration(duration)}", image: user_avatar || '' @@ -45,7 +45,7 @@ module ChatMessage private def message - "#{project_link}: Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_name} #{humanized_status} in #{pretty_duration(duration)}" + "#{project_link}: Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_combined_name} #{humanized_status} in #{pretty_duration(duration)}" end def humanized_status diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb index c52dd6ef8ef..8d599c5f116 100644 --- a/app/models/project_services/chat_message/push_message.rb +++ b/app/models/project_services/chat_message/push_message.rb @@ -33,7 +33,7 @@ module ChatMessage end { - title: "#{user_name} #{action} #{ref_type}", + title: "#{user_combined_name} #{action} #{ref_type}", subtitle: "in #{project_link}", text: compare_link, image: user_avatar @@ -57,15 +57,15 @@ module ChatMessage end def new_branch_message - "#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}" + "#{user_combined_name} pushed new #{ref_type} #{branch_link} to #{project_link}" end def removed_branch_message - "#{user_name} removed #{ref_type} #{ref} from #{project_link}" + "#{user_combined_name} removed #{ref_type} #{ref} from #{project_link}" end def push_message - "#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})" + "#{user_combined_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})" end def commit_messages diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb index a139a8ee727..d84b80f2de2 100644 --- a/app/models/project_services/chat_message/wiki_page_message.rb +++ b/app/models/project_services/chat_message/wiki_page_message.rb @@ -31,7 +31,7 @@ module ChatMessage def activity { - title: "#{user_name} #{action} #{wiki_page_link}", + title: "#{user_combined_name} #{action} #{wiki_page_link}", subtitle: "in #{project_link}", text: title, image: user_avatar @@ -41,7 +41,7 @@ module ChatMessage private def message - "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*" + "#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*" end def description_message diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index 298569cb7a6..6e311806be1 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -53,13 +53,17 @@ class SentNotification < ActiveRecord::Base end def unsubscribable? - !for_commit? + !(for_commit? || for_snippet?) end def for_commit? noteable_type == "Commit" end + def for_snippet? + noteable_type.end_with?('Snippet') + end + def noteable if for_commit? project.commit(commit_id) rescue nil diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 0b33e45473b..1f9f8d7286b 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -2,7 +2,7 @@ class SystemNoteMetadata < ActiveRecord::Base ICON_TYPES = %w[ commit description merge confidential visible label assignee cross_reference title time_tracking branch milestone discussion task moved - opened closed merged duplicate + opened closed merged duplicate locked unlocked outdated ].freeze diff --git a/app/models/user.rb b/app/models/user.rb index 4e71a3e11c2..533a776bc65 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -163,15 +163,16 @@ 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? - - after_update :update_emails_with_primary_email, if: :email_changed? before_save :ensure_authentication_token, :ensure_incoming_email_token before_save :ensure_user_rights_and_limits, if: :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_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') } + after_initialize :set_projects_limit - after_destroy :post_destroy_hook # User's Layout preference enum layout: [:fixed, :fluid] @@ -458,6 +459,14 @@ class User < ActiveRecord::Base reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago end + def remember_me! + super if ::Gitlab::Database.read_write? + end + + def forget_me! + super if ::Gitlab::Database.read_write? + end + def disable_two_factor! transaction do update_attributes( @@ -525,12 +534,24 @@ class User < ActiveRecord::Base errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email) end + # see if the new email is already a verified secondary email + def check_for_verified_email + skip_reconfirmation! if emails.confirmed.where(email: self.email).any? + end + + # Note: the use of the Emails services will cause `saves` on the user object, running + # through the callbacks again and can have side effects, such as the `previous_changes` + # hash and `_was` variables getting munged. + # By using an `after_commit` instead of `after_update`, we avoid the recursive callback + # scenario, though it then requires us to use the `previous_changes` hash def update_emails_with_primary_email + previous_email = previous_changes[:email][0] # grab this before the DestroyService is called primary_email_record = emails.find_by(email: email) - if primary_email_record - Emails::DestroyService.new(self, user: self, email: email).execute - Emails::CreateService.new(self, user: self, email: email_was).execute - end + Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record + + # the original primary email was confirmed, and we want that to carry over. We don't + # have access to the original confirmation values at this point, so just set confirmed_at + Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at) end def update_invalid_gpg_signatures @@ -641,6 +662,10 @@ class User < ActiveRecord::Base Ability.allowed?(self, action, subject) end + def confirm_deletion_with_password? + !password_automatically_set? && allow_password_authentication? + end + def first_name name.split.first unless name.blank? end @@ -680,15 +705,7 @@ class User < ActiveRecord::Base end def fork_of(project) - links = ForkedProjectLink.where( - forked_from_project_id: project, - forked_to_project_id: personal_projects.unscope(:order) - ) - if links.any? - links.first.forked_to_project - else - nil - end + namespace.find_fork_of(project) end def ldap_user? @@ -816,6 +833,10 @@ class User < ActiveRecord::Base avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username) end + def primary_email_verified? + confirmed? && !temp_oauth_email? + end + def all_emails all_emails = [] all_emails << email unless temp_oauth_email? @@ -823,6 +844,18 @@ class User < ActiveRecord::Base all_emails end + def verified_emails + verified_emails = [] + verified_emails << email if primary_email_verified? + verified_emails.concat(emails.confirmed.pluck(:email)) + verified_emails + end + + def verified_email?(check_email) + downcased = check_email.downcase + email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists? + end + def hook_attrs { name: name, @@ -1047,10 +1080,6 @@ class User < ActiveRecord::Base ensure_rss_token! end - def verified_email?(email) - self.email == email - end - def sync_attribute?(attribute) return true if ldap_user? && attribute == :email diff --git a/app/policies/gcp/cluster_policy.rb b/app/policies/gcp/cluster_policy.rb new file mode 100644 index 00000000000..e77173ea6e1 --- /dev/null +++ b/app/policies/gcp/cluster_policy.rb @@ -0,0 +1,12 @@ +module Gcp + class ClusterPolicy < BasePolicy + alias_method :cluster, :subject + + delegate { @subject.project } + + rule { can?(:master_access) }.policy do + enable :update_cluster + enable :admin_cluster + end + end +end diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index daf6fa9e18a..f0aa16d2ecf 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -1,6 +1,10 @@ class IssuablePolicy < BasePolicy delegate { @subject.project } + condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? } + + condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) } + desc "User is the assignee or author" condition(:assignee_or_author) do @user && @subject.assignee_or_author?(@user) @@ -12,4 +16,12 @@ class IssuablePolicy < BasePolicy enable :read_merge_request enable :update_merge_request end + + rule { locked & ~is_project_member }.policy do + prevent :create_note + prevent :update_note + prevent :admin_note + prevent :resolve_note + prevent :edit_note + end end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index 20cd51cfb99..d4cb5a77e63 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -1,5 +1,6 @@ class NotePolicy < BasePolicy delegate { @subject.project } + delegate { @subject.noteable if @subject.noteable.lockable? } condition(:is_author) { @user && @subject.author == @user } condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? } @@ -8,6 +9,7 @@ class NotePolicy < BasePolicy condition(:editable, scope: :subject) { @subject.editable? } rule { ~editable | anonymous }.prevent :edit_note + rule { is_author | admin }.enable :edit_note rule { can?(:master_access) }.enable :edit_note diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index b7b5bd34189..f599eab42f2 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -193,6 +193,8 @@ class ProjectPolicy < BasePolicy enable :admin_pages enable :read_pages enable :update_pages + enable :read_cluster + enable :create_cluster end rule { can?(:public_user_access) }.policy do diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb index a542bdd8295..099b4720fb6 100644 --- a/app/presenters/ci/pipeline_presenter.rb +++ b/app/presenters/ci/pipeline_presenter.rb @@ -1,7 +1,18 @@ module Ci class PipelinePresenter < Gitlab::View::Presenter::Delegated + FAILURE_REASONS = { + config_error: 'CI/CD YAML configuration error!' + }.freeze + presents :pipeline + def failure_reason + return unless pipeline.failure_reason? + + FAILURE_REASONS[pipeline.failure_reason.to_sym] || + pipeline.failure_reason + end + def status_title if auto_canceled? "Pipeline is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}" diff --git a/app/presenters/gcp/cluster_presenter.rb b/app/presenters/gcp/cluster_presenter.rb new file mode 100644 index 00000000000..f7908f92a37 --- /dev/null +++ b/app/presenters/gcp/cluster_presenter.rb @@ -0,0 +1,9 @@ +module Gcp + 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}" + end + end +end diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb new file mode 100644 index 00000000000..08a113c4d8a --- /dev/null +++ b/app/serializers/cluster_entity.rb @@ -0,0 +1,6 @@ +class ClusterEntity < Grape::Entity + include RequestAwareEntity + + expose :status_name, as: :status + expose :status_reason +end diff --git a/app/serializers/cluster_serializer.rb b/app/serializers/cluster_serializer.rb new file mode 100644 index 00000000000..2c87202a105 --- /dev/null +++ b/app/serializers/cluster_serializer.rb @@ -0,0 +1,7 @@ +class ClusterSerializer < BaseSerializer + entity ClusterEntity + + def represent_status(resource) + represent(resource, { only: [:status, :status_reason] }) + end +end diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb index e4e9d8ef90a..c8dd98cc04d 100644 --- a/app/serializers/commit_entity.rb +++ b/app/serializers/commit_entity.rb @@ -1,4 +1,4 @@ -class CommitEntity < API::Entities::RepoCommit +class CommitEntity < API::Entities::Commit include RequestAwareEntity expose :author, using: UserEntity 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/issue_entity.rb b/app/serializers/issue_entity.rb index 0d6feb78173..10d3ad0214b 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -3,6 +3,7 @@ class IssueEntity < IssuableEntity expose :branch_name expose :confidential + expose :discussion_locked expose :assignees, using: API::Entities::UserBasic expose :due_date expose :moved_to_id @@ -14,7 +15,7 @@ class IssueEntity < IssuableEntity expose :current_user do expose :can_create_note do |issue| - can?(request.current_user, :create_note, issue.project) + can?(request.current_user, :create_note, issue) end expose :can_update do |issue| diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb index 36537c5bd02..297a459e394 100644 --- a/app/serializers/merge_request_entity.rb +++ b/app/serializers/merge_request_entity.rb @@ -42,6 +42,7 @@ class MergeRequestEntity < IssuableEntity expose :commits_count expose :cannot_be_merged?, as: :has_conflicts expose :can_be_merged?, as: :can_be_merged + expose :mergeable?, as: :mergeable expose :remove_source_branch?, as: :remove_source_branch expose :project_archived do |merge_request| diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index 357fc71f877..6457294b285 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -20,6 +20,7 @@ class PipelineEntity < Grape::Entity expose :has_yaml_errors?, as: :yaml_errors expose :can_retry?, as: :retryable expose :can_cancel?, as: :cancelable + expose :failure_reason?, as: :failure_reason end expose :details do @@ -44,6 +45,11 @@ class PipelineEntity < Grape::Entity end expose :commit, using: CommitEntity + expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? } + + expose :failure_reason, if: -> (pipeline, _) { pipeline.failure_reason? } do |pipeline| + pipeline.present.failure_reason + end expose :retry_path, if: -> (*) { can_retry? } do |pipeline| retry_project_pipeline_path(pipeline.project, pipeline) @@ -53,8 +59,6 @@ class PipelineEntity < Grape::Entity cancel_project_pipeline_path(pipeline.project, pipeline) end - expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? } - private alias_method :pipeline, :object diff --git a/app/services/ci/create_cluster_service.rb b/app/services/ci/create_cluster_service.rb new file mode 100644 index 00000000000..f7ee0e468e2 --- /dev/null +++ b/app/services/ci/create_cluster_service.rb @@ -0,0 +1,15 @@ +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/extract_sections_from_build_trace_service.rb b/app/services/ci/extract_sections_from_build_trace_service.rb new file mode 100644 index 00000000000..75f9e0f897d --- /dev/null +++ b/app/services/ci/extract_sections_from_build_trace_service.rb @@ -0,0 +1,30 @@ +module Ci + class ExtractSectionsFromBuildTraceService < BaseService + def execute(build) + return false unless build.trace_sections.empty? + + Gitlab::Database.bulk_insert(BuildTraceSection.table_name, extract_sections(build)) + true + end + + private + + def find_or_create_name(name) + project.build_trace_section_names.find_or_create_by!(name: name) + rescue ActiveRecord::RecordInvalid + project.build_trace_section_names.find_by!(name: name) + end + + def extract_sections(build) + build.trace.extract_sections.map do |attr| + name = attr.delete(:name) + name_record = find_or_create_name(name) + + attr.merge( + build_id: build.id, + project_id: project.id, + section_name_id: name_record.id) + end + end + end +end diff --git a/app/services/ci/fetch_gcp_operation_service.rb b/app/services/ci/fetch_gcp_operation_service.rb new file mode 100644 index 00000000000..0b68e4d6ea9 --- /dev/null +++ b/app/services/ci/fetch_gcp_operation_service.rb @@ -0,0 +1,17 @@ +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/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb new file mode 100644 index 00000000000..44da87cb00c --- /dev/null +++ b/app/services/ci/fetch_kubernetes_token_service.rb @@ -0,0 +1,72 @@ +## +# TODO: +# Almost components in this class were copied from app/models/project_services/kubernetes_service.rb +# We should dry up those classes not to repeat the same code. +# Maybe we should have a special facility (e.g. lib/kubernetes_api) to maintain all Kubernetes API caller. +module Ci + class FetchKubernetesTokenService + attr_reader :api_url, :ca_pem, :username, :password + + def initialize(api_url, ca_pem, username, password) + @api_url = api_url + @ca_pem = ca_pem + @username = username + @password = password + end + + def execute + read_secrets.each do |secret| + name = secret.dig('metadata', 'name') + if /default-token/ =~ name + token_base64 = secret.dig('data', 'token') + return Base64.decode64(token_base64) if token_base64 + end + end + + nil + end + + private + + def read_secrets + kubeclient = build_kubeclient! + + kubeclient.get_secrets.as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + end + + def build_kubeclient!(api_path: 'api', api_version: 'v1') + raise "Incomplete settings" unless api_url && username && password + + ::Kubeclient::Client.new( + join_api_url(api_path), + api_version, + auth_options: { username: username, password: password }, + ssl_options: kubeclient_ssl_options, + http_proxy_uri: ENV['http_proxy'] + ) + end + + def join_api_url(api_path) + url = URI.parse(api_url) + prefix = url.path.sub(%r{/+\z}, '') + + url.path = [prefix, api_path].join("/") + + url.to_s + end + + def kubeclient_ssl_options + opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } + + if ca_pem.present? + opts[:cert_store] = OpenSSL::X509::Store.new + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + end + + opts + end + end +end diff --git a/app/services/ci/finalize_cluster_creation_service.rb b/app/services/ci/finalize_cluster_creation_service.rb new file mode 100644 index 00000000000..347875c5697 --- /dev/null +++ b/app/services/ci/finalize_cluster_creation_service.rb @@ -0,0 +1,33 @@ +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 new file mode 100644 index 00000000000..d123ce8d26b --- /dev/null +++ b/app/services/ci/integrate_cluster_service.rb @@ -0,0 +1,26 @@ +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 new file mode 100644 index 00000000000..52d80b01813 --- /dev/null +++ b/app/services/ci/provision_cluster_service.rb @@ -0,0 +1,36 @@ +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/update_cluster_service.rb b/app/services/ci/update_cluster_service.rb new file mode 100644 index 00000000000..70d88fca660 --- /dev/null +++ b/app/services/ci/update_cluster_service.rb @@ -0,0 +1,22 @@ +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/emails/base_service.rb b/app/services/emails/base_service.rb index 7f591c89411..5bbceeb3b3f 100644 --- a/app/services/emails/base_service.rb +++ b/app/services/emails/base_service.rb @@ -1,9 +1,8 @@ module Emails class BaseService - def initialize(current_user, opts) - @current_user = current_user - @user = opts.delete(:user) - @email = opts[:email] + def initialize(current_user, params = {}) + @current_user, @params = current_user, params.dup + @user = params.delete(:user) end end end diff --git a/app/services/emails/confirm_service.rb b/app/services/emails/confirm_service.rb new file mode 100644 index 00000000000..b5301bf2b82 --- /dev/null +++ b/app/services/emails/confirm_service.rb @@ -0,0 +1,7 @@ +module Emails + class ConfirmService < ::Emails::BaseService + def execute(email) + email.resend_confirmation_instructions + end + end +end diff --git a/app/services/emails/create_service.rb b/app/services/emails/create_service.rb index b6491ee9804..94a841af7c3 100644 --- a/app/services/emails/create_service.rb +++ b/app/services/emails/create_service.rb @@ -1,7 +1,7 @@ module Emails class CreateService < ::Emails::BaseService - def execute - @user.emails.create(email: @email) + def execute(extra_params = {}) + @user.emails.create(@params.merge(extra_params)) end end end diff --git a/app/services/emails/destroy_service.rb b/app/services/emails/destroy_service.rb index 44011cc36c8..1ed131fe326 100644 --- a/app/services/emails/destroy_service.rb +++ b/app/services/emails/destroy_service.rb @@ -1,7 +1,7 @@ module Emails class DestroyService < ::Emails::BaseService - def execute - update_secondary_emails! if Email.find_by_email!(@email).destroy + def execute(email) + email.destroy && update_secondary_emails! end private diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 12604e7eb5d..d61a342ebad 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -43,6 +43,10 @@ class IssuableBaseService < BaseService 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}" @@ -57,6 +61,7 @@ class IssuableBaseService < BaseService params.delete(:due_date) params.delete(:canonical_issue_id) params.delete(:project) + params.delete(:discussion_locked) end filter_assignee(issuable) @@ -236,6 +241,7 @@ class IssuableBaseService < BaseService handle_common_system_notes(issuable, old_labels: old_labels) end + change_discussion_lock(issuable) handle_changes( issuable, old_labels: old_labels, @@ -249,7 +255,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 @@ -294,6 +300,12 @@ 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 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/update_service.rb b/app/services/issues/update_service.rb index b4ca3966505..e0339ddf9bb 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -34,7 +34,7 @@ module Issues if issue.assignees != old_assignees create_assignee_note(issue, old_assignees) notification_service.reassigned_issue(issue, current_user, old_assignees) - todo_service.reassigned_issue(issue, current_user) + todo_service.reassigned_issue(issue, current_user, old_assignees) end if issue.previous_changes.include?('confidential') diff --git a/app/services/keys/last_used_service.rb b/app/services/keys/last_used_service.rb index 066f3246158..dbd79f7da55 100644 --- a/app/services/keys/last_used_service.rb +++ b/app/services/keys/last_used_service.rb @@ -16,6 +16,8 @@ module Keys end def update? + return false if ::Gitlab::Database.read_only? + last_used = key.last_used_at return false if last_used && (Time.zone.now - last_used) <= TIMEOUT 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/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/notification_service.rb b/app/services/notification_service.rb index e2a80db06a6..8d5da459882 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -31,13 +31,6 @@ class NotificationService end end - # Always notify user about email added to profile - def new_email(email) - if email.user&.can?(:receive_notifications) - mailer.new_email_email(email.id).deliver_later - end - end - # When create an issue we should send an email to: # # * issue assignee if their notification level is not Disabled diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 54eb75ab9bf..19d75ff2efa 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -22,6 +22,13 @@ module Projects Projects::UnlinkForkService.new(project, current_user).execute + # The project is not necessarily a fork, so update the fork network originating + # from this project + if fork_network = project.root_of_fork_network + fork_network.update(root_project: nil, + deleted_root_project_name: project.full_name) + end + attempt_destroy_transaction(project) system_hook_service.execute_hooks_for(project, :destroy) diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index ad67e68a86a..eb5cce5ab98 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -23,11 +23,31 @@ module Projects refresh_forks_count + link_fork_network(new_project) + new_project end private + def fork_network + if @project.fork_network + @project.fork_network + elsif forked_from_project = @project.forked_from_project + # TODO: remove this case when all background migrations have completed + # this only happens when a project had a `forked_project_link` that was + # not migrated to the `fork_network` relation + forked_from_project.fork_network || forked_from_project.create_root_of_fork_network + else + @project.create_root_of_fork_network + end + end + + def link_fork_network(new_project) + fork_network.fork_network_members.create(project: new_project, + forked_from_project: @project) + end + def refresh_forks_count Projects::ForksCountService.new(@project).refresh_cache end diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index f30b40423c8..abe414d0c05 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -16,6 +16,7 @@ module Projects refresh_forks_count(@project.forked_from_project) @project.forked_project_link.destroy + @project.fork_network_member.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..955d934838b 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -458,7 +458,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_note_service.rb b/app/services/system_note_service.rb index 1f66a2668f9..a52dce6cb4b 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -591,6 +591,13 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate')) end + def discussion_lock(issuable, author) + action = issuable.discussion_locked? ? 'locked' : 'unlocked' + body = "#{action} this #{issuable.class.to_s.titleize.downcase}" + + create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action)) + end + private def notes_for_mentioner(mentioner, noteable, notes) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 6ee96d6a0f8..b6125cafa83 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -43,8 +43,8 @@ class TodoService # # * create a pending todo for new assignee if issue is assigned # - def reassigned_issue(issue, current_user) - create_assignment_todo(issue, current_user) + def reassigned_issue(issue, current_user, old_assignees = []) + create_assignment_todo(issue, current_user, old_assignees) end # When create a merge request we should: @@ -254,10 +254,11 @@ class TodoService create_mention_todos(project, target, author, note, skip_users) end - def create_assignment_todo(issuable, author) + def create_assignment_todo(issuable, author, old_assignees = []) if issuable.assignees.any? + assignees = issuable.assignees - old_assignees attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED) - create_todos(issuable.assignees, attributes) + create_todos(assignees, attributes) end end diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb index ab532a1fdcf..5803404c3c8 100644 --- a/app/services/users/activity_service.rb +++ b/app/services/users/activity_service.rb @@ -14,7 +14,7 @@ module Users private def record_activity - Gitlab::UserActivities.record(@author.id) + Gitlab::UserActivities.record(@author.id) if Gitlab::Database.read_write? Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})") end 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/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml index 0310498ae54..7066ed12b95 100644 --- a/app/views/admin/jobs/index.html.haml +++ b/app/views/admin/jobs/index.html.haml @@ -3,7 +3,7 @@ %div{ class: container_class } - .top-area + .top-area.scrolling-tabs-container.inner-page-scroll-tabs - build_path_proc = ->(scope) { admin_jobs_path(scope: scope) } = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 3f202fbf4fe..4d8754afdd2 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -4,7 +4,7 @@ %div{ class: container_class } - .top-area + .top-area.scrolling-tabs-container.inner-page-scroll-tabs .prepend-top-default .search-holder = render 'shared/projects/search_form', autofocus: true, icon: true diff --git a/app/views/devise/mailer/_confirmation_instructions_account.html.haml b/app/views/devise/mailer/_confirmation_instructions_account.html.haml new file mode 100644 index 00000000000..65565b7b8a8 --- /dev/null +++ b/app/views/devise/mailer/_confirmation_instructions_account.html.haml @@ -0,0 +1,16 @@ +- confirmation_link = confirmation_url(@resource, confirmation_token: @token) +- if @resource.unconfirmed_email.present? + #content + = email_default_heading(@resource.unconfirmed_email) + %p Click the link below to confirm your email address. + #cta + = link_to 'Confirm your email address', confirmation_link +- else + #content + - if Gitlab.com? + = email_default_heading('Thanks for signing up to GitLab!') + - else + = email_default_heading("Welcome, #{@resource.name}!") + %p To get started, click the link below to confirm your account. + #cta + = link_to 'Confirm your account', confirmation_link diff --git a/app/views/devise/mailer/_confirmation_instructions_account.text.erb b/app/views/devise/mailer/_confirmation_instructions_account.text.erb new file mode 100644 index 00000000000..01f09aa763d --- /dev/null +++ b/app/views/devise/mailer/_confirmation_instructions_account.text.erb @@ -0,0 +1,14 @@ +<% if @resource.unconfirmed_email.present? %> +<%= @resource.unconfirmed_email %>, + +Use the link below to confirm your email address. +<% else %> + <% if Gitlab.com? %> +Thanks for signing up to GitLab! + <% else %> +Welcome, <%= @resource.name %>! + <% end %> +To get started, use the link below to confirm your account. +<% end %> + +<%= confirmation_url(@resource, confirmation_token: @token) %> diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml new file mode 100644 index 00000000000..3d0a1f622a5 --- /dev/null +++ b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml @@ -0,0 +1,8 @@ +#content + = email_default_heading("#{@resource.user.name}, you've added an additional email!") + %p Click the link below to confirm your email address (#{@resource.email}) + #cta + = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) + %p + If this email was added in error, you can remove it here: + = link_to "Emails", profile_emails_url diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb new file mode 100644 index 00000000000..a3b28cb0b84 --- /dev/null +++ b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb @@ -0,0 +1,7 @@ +<%= @resource.user.name %>, you've added an additional email! + +Use the link below to confirm your email address (<%= @resource.email %>) + +<%= confirmation_url(@resource, confirmation_token: @token) %> + +If this email was added in error, you can remove it here: <%= profile_emails_url %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.haml b/app/views/devise/mailer/confirmation_instructions.html.haml index 4d1037807be..50ee7b53d8f 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.haml +++ b/app/views/devise/mailer/confirmation_instructions.html.haml @@ -1,16 +1 @@ -- confirmation_link = confirmation_url(@resource, confirmation_token: @token) -- if @resource.unconfirmed_email.present? - #content - = email_default_heading(@resource.unconfirmed_email) - %p Click the link below to confirm your email address. - #cta - = link_to confirmation_link, confirmation_link -- else - #content - - if Gitlab.com? - = email_default_heading('Thanks for signing up to GitLab!') - - else - = email_default_heading("Welcome, #{@resource.name}!") - %p To get started, click the link below to confirm your account. - #cta - = link_to confirmation_link, confirmation_link += render partial: "confirmation_instructions_#{@resource.is_a?(User) ? 'account' : 'secondary'}" diff --git a/app/views/devise/mailer/confirmation_instructions.text.erb b/app/views/devise/mailer/confirmation_instructions.text.erb index 9f76edb76a4..05fddddf415 100644 --- a/app/views/devise/mailer/confirmation_instructions.text.erb +++ b/app/views/devise/mailer/confirmation_instructions.text.erb @@ -1,9 +1 @@ -Welcome, <%= @resource.name %>! - -<% if @resource.unconfirmed_email.present? %> -You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below: -<% else %> -You can confirm your account through the link below: -<% end %> - -<%= confirmation_url(@resource, confirmation_token: @token) %> +<%= render partial: "confirmation_instructions_#{@resource.is_a?(User) ? 'account' : 'secondary'}" %>
\ No newline at end of file diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml index e6d307e5568..4b6c4581eb3 100644 --- a/app/views/discussions/_diff_discussion.html.haml +++ b/app/views/discussions/_diff_discussion.html.haml @@ -1,6 +1,10 @@ -- expanded = local_assigns.fetch(:expanded, true) -%tr.notes_holder{ class: ('hide' unless expanded) } - %td.notes_line{ colspan: 2 } - %td.notes_content - .content{ class: ('hide' unless expanded) } - = render partial: "discussions/notes", collection: discussions, as: :discussion +- if local_assigns[:on_image] + = render partial: "discussions/notes", collection: discussions, as: :discussion +- else + -# Text diff discussions + - expanded = local_assigns.fetch(:expanded, true) + %tr.notes_holder{ class: ('hide' unless expanded) } + %td.notes_line{ colspan: 2 } + %td.notes_content + .content{ class: ('hide' unless expanded) } + = 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 4a41be972da..f9bfc01f213 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -1,18 +1,27 @@ - diff_file = discussion.diff_file - blob = discussion.blob +- discussions = { discussion.original_line_code => [discussion] } +- diff_file_class = diff_file.text? ? 'text-file' : 'js-image-file' -.diff-file.file-holder +.diff-file.file-holder{ class: diff_file_class } .js-file-title.file-title.file-title-flex-parent .file-header-content = render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false - .diff-content.code.js-syntax-highlight - %table - - discussions = { discussion.original_line_code => [discussion] } - = render partial: "projects/diffs/line", - collection: discussion.truncated_diff_lines, - as: :line, - locals: { diff_file: diff_file, - discussions: discussions, - discussion_expanded: true, - plain: true } + - if diff_file.text? + .diff-content.code.js-syntax-highlight + %table + = render partial: "projects/diffs/line", + collection: discussion.truncated_diff_lines, + as: :line, + locals: { diff_file: diff_file, + discussions: discussions, + discussion_expanded: true, + plain: true } + - else + - partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff' + + = 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_class: true } diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml index db5ab939948..1cc227428e9 100644 --- a/app/views/discussions/_notes.html.haml +++ b/app/views/discussions/_notes.html.haml @@ -1,6 +1,19 @@ -.discussion-notes - %ul.notes{ data: { discussion_id: discussion.id } } - = render partial: "shared/notes/note", collection: discussion.notes, as: :note +- 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) + +.discussion-notes{ class: collapsed_class } + -# Save the first note position data so that we have a reference and can go + -# to the first note position when we click on a badge diff discussion + %ul.notes{ id: "discussion_#{discussion.id}", data: { discussion_id: discussion.id, position: discussion.notes[0].position.to_json } } + - if discussion.try(:on_image?) && show_toggle + %button.diff-notes-collapse.js-diff-notes-toggle{ type: 'button' } + = sprite_icon('collapse', css_class: 'collapse-icon') + %button.btn-transparent.badge.js-diff-notes-toggle{ type: 'button' } + = badge_counter + = render partial: "shared/notes/note", collection: discussion.notes, as: :note, locals: { badge_counter: badge_counter, show_image_comment_badge: show_image_comment_badge } .flash-container diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 53ebdd6d2ff..9a763887b30 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -19,8 +19,7 @@ - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user) - if event.commits_count > 1 %li.commits-stat - - if event.commits_count > 2 - %span ... and #{event.commits_count - 2} more commits. + %span ... and #{pluralize(event.commits_count - 1, 'more commit')}. - if event.md_ref? - from = event.commit_from 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/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/milestones/_header_title.html.haml b/app/views/groups/milestones/_header_title.html.haml index d7fabf53587..24eb39b8e2f 100644 --- a/app/views/groups/milestones/_header_title.html.haml +++ b/app/views/groups/milestones/_header_title.html.haml @@ -1 +1,2 @@ -- header_title group_title(@group, "Milestones", group_milestones_path(@group)) +- breadcrumb_title @milestone.title +- add_to_breadcrumbs "Milestones", group_milestones_path(@group) diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 0d5350f873b..f1b32274664 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") + = javascript_include_tag asset_path("locale/#{I18n.locale.to_s || I18n.default_locale.to_s}/app.js") unless I18n.locale == :en = webpack_bundle_tag "webpack_runtime" = webpack_bundle_tag "common" = webpack_bundle_tag "main" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 7e9b76da570..5ff6ac5fc00 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,4 +1,4 @@ -%header.navbar.navbar-gitlab.navbar-gitlab-new +%header.navbar.navbar-gitlab %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid .header-content @@ -73,7 +73,7 @@ %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' } %span.sr-only Toggle navigation - = sprite_icon('more', size: 16, css_class: 'more-icon js-navbar-toggle-right') - = sprite_icon('close', size: 16, css_class: 'close-icon js-navbar-toggle-left') + = sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right') + = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') = render 'shared/outdated_browser' diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml index 610ff9001f7..ad0d51d28f9 100644 --- a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml +++ b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml @@ -4,7 +4,7 @@ %li.dropdown %button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip } = icon("ellipsis-h") - = sprite_icon("angle-right", css_class: "breadcrumbs-list-angle") + = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle") .dropdown-menu %ul - @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index| 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 8765b814405..759d6ff68ea 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -146,7 +146,7 @@ = number_with_delimiter(@project.open_merge_requests_count) - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do + = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters]) do = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do .nav-icon-container = sprite_icon('pipeline') @@ -189,6 +189,12 @@ %span Charts + - if project_nav_tab? :clusters + = nav_link(controller: :clusters) do + = link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do + %span + Cluster + - if project_nav_tab? :wiki = nav_link(controller: :wikis) do = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do diff --git a/app/views/notify/new_email_email.html.haml b/app/views/notify/new_email_email.html.haml deleted file mode 100644 index 4a0448a573c..00000000000 --- a/app/views/notify/new_email_email.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%p - Hi #{@user.name}! -%p - A new email was added to your account: -%p - email: - %code= @email.email -%p - If this email was added in error, you can remove it here: - = link_to "Emails", profile_emails_url diff --git a/app/views/notify/new_email_email.text.erb b/app/views/notify/new_email_email.text.erb deleted file mode 100644 index 51cba99ad0d..00000000000 --- a/app/views/notify/new_email_email.text.erb +++ /dev/null @@ -1,7 +0,0 @@ -Hi <%= @user.name %>! - -A new email was added to your account: - -email.................. <%= @email.email %> - -If this email was added in error, you can remove it here: <%= profile_emails_url %> diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 8abbd828032..7f79168dfb3 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -97,21 +97,29 @@ .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0.danger-title - Remove account + = s_('Profiles|Delete account') .col-lg-8 - if @user.can_be_removed? && can?(current_user, :destroy_user, @user) %p - Deleting an account has the following effects: + = s_('Profiles|Deleting an account has the following effects:') = render 'users/deletion_guidance', user: current_user - = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" + + #delete-account-modal{ data: { action_url: user_registration_path, + confirm_with_password: ('true' if current_user.confirm_deletion_with_password?), + username: current_user.username } } + %button.btn.btn-danger.disabled + = s_('Profiles|Delete account') - else - if @user.solo_owned_groups.present? %p - Your account is currently an owner in these groups: + = s_('Profiles|Your account is currently an owner in these groups:') %strong= @user.solo_owned_groups.map(&:name).join(', ') %p - You must transfer ownership or delete these groups before you can delete your account. + = s_('Profiles|You must transfer ownership or delete these groups before you can delete your account.') - else %p - You don't have access to delete this user. + = s_("Profiles|You don't have access to delete this user.") .append-bottom-default + +- content_for :page_specific_javascripts do + = webpack_bundle_tag('account') diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 612ecbbb96a..df1df4f5d72 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -32,19 +32,25 @@ All email addresses will be used to identify your commits. %ul.well-list %li - = @primary + = render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? } %span.pull-right %span.label.label-success Primary email - - if @primary === current_user.public_email + - if @primary_email === current_user.public_email %span.label.label-info Public email - - if @primary === current_user.notification_email + - if @primary_email === current_user.notification_email %span.label.label-info Notification email - @emails.each do |email| %li - = email.email + = render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? } %span.pull-right - if email.email === current_user.public_email %span.label.label-info Public email - if email.email === current_user.notification_email %span.label.label-info Notification email - = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-warning prepend-left-10' + - unless email.confirmed? + - confirm_title = "#{email.confirmation_sent_at ? 'Resend' : 'Send'} confirmation email" + = link_to confirm_title, resend_confirmation_instructions_profile_email_path(email), method: :put, class: 'btn btn-sm btn-warning prepend-left-10' + + = link_to profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-danger prepend-left-10' do + %span.sr-only Remove + = icon('trash') diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml index b04981f90e3..5ed517c1ef6 100644 --- a/app/views/profiles/gpg_keys/_key.html.haml +++ b/app/views/profiles/gpg_keys/_key.html.haml @@ -3,10 +3,17 @@ = icon 'key', class: "settings-list-icon hidden-xs" .key-list-item-info - key.emails_with_verified_status.map do |email, verified| - = render partial: 'email_with_badge', locals: { email: email, verified: verified } + = render partial: 'shared/email_with_badge', locals: { email: email, verified: verified } .description %code= key.fingerprint + - if key.subkeys.present? + .subkeys + %span.bold Subkeys: + %ul.subkeys-list + - key.subkeys.each do |subkey| + %li + %code= subkey.fingerprint .pull-right %span.key-created-at created #{time_ago_with_tooltip(key.created_at)} diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 873b3045ea9..619b632918e 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,4 +1,6 @@ - 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 @@ -12,11 +14,15 @@ - if @project.description.present? = markdown_field(@project, :description) - - if forked_from_project = @project.forked_from_project + - if @project.forked? %p - #{ s_('ForkedFromProjectPath|Forked from') } - = link_to project_path(forked_from_project) do - = forked_from_project.namespace.try(:name) + - if forked_from_project + #{ s_('ForkedFromProjectPath|Forked from') } + = link_to project_path(forked_from_project) do + = forked_from_project.full_name + - else + - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') + = deleted_message % { project_name: fork_network.deleted_root_project_name } .project-repo-buttons .count-buttons diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 71424593f2e..770608eddff 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,5 +1,12 @@ - referenced_users = local_assigns.fetch(:referenced_users, nil) +- if defined?(@merge_request) && @merge_request.discussion_locked? + .issuable-note-warning + = icon('lock', class: 'icon') + %span + = _('This merge request is locked.') + = _('Only project members can comment.') + .md-area .md-header %ul.nav-links.clearfix diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml index 8edb9be049a..a97ddb3c377 100644 --- a/app/views/projects/artifacts/_tree_file.html.haml +++ b/app/views/projects/artifacts/_tree_file.html.haml @@ -1,10 +1,17 @@ +- blob = file.blob - path_to_file = file_project_job_artifacts_path(@project, @build, path: file.path) +- external_link = blob.external_link?(@build) -%tr.tree-item{ 'data-link' => path_to_file } - - blob = file.blob +%tr.tree-item.js-artifact-tree-row{ data: { link: path_to_file, external_link: "#{external_link}" } } %td.tree-item-file-name = tree_icon('file', blob.mode, blob.name) - = link_to path_to_file do - %span.str-truncated= blob.name + - if external_link + = link_to path_to_file, class: 'tree-item-file-external-link js-artifact-tree-tooltip', + target: '_blank', rel: 'noopener noreferrer', title: _('Opens in a new window') do + %span.str-truncated>= blob.name + = icon('external-link', class: 'js-artifact-tree-external-icon') + - else + = link_to path_to_file do + %span.str-truncated= blob.name %td = number_to_human_size(blob.size, precision: 2) diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml new file mode 100644 index 00000000000..371cdb1e403 --- /dev/null +++ b/app/views/projects/clusters/_form.html.haml @@ -0,0 +1,37 @@ +.row + .col-sm-8.col-sm-offset-4 + %p + - 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_errors(@cluster) + .form-group + = field.label :gcp_cluster_name, s_('ClusterIntegration|Cluster name') + = field.text_field :gcp_cluster_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' + + .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 + = 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 + = field.label :project_namespace, s_('ClusterIntegration|Project namespace (optional, unique)') + = field.text_field :project_namespace, class: 'form-control', placeholder: @cluster.project_namespace_placeholder + + .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 new file mode 100644 index 00000000000..0134d46491c --- /dev/null +++ b/app/views/projects/clusters/_header.html.haml @@ -0,0 +1,14 @@ +%h4.prepend-top-0 + = s_('ClusterIntegration|Create new cluster on Google Container Engine') +%p + = s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:') +%ul + %li + - link_to_container_engine = link_to(s_('ClusterIntegration|access to Google Container Engine'), 'https://console.cloud.google.com', target: '_blank', rel: 'noopener noreferrer') + = s_('ClusterIntegration|Your account must have %{link_to_container_engine}').html_safe % { link_to_container_engine: link_to_container_engine } + %li + - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/container-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer') + = 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 } diff --git a/app/views/projects/clusters/_sidebar.html.haml b/app/views/projects/clusters/_sidebar.html.haml new file mode 100644 index 00000000000..761879db32b --- /dev/null +++ b/app/views/projects/clusters/_sidebar.html.haml @@ -0,0 +1,7 @@ +%h4.prepend-top-0 + = s_('ClusterIntegration|Cluster integration') +%p + = s_('ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.') +%p + - link = link_to(s_('ClusterIntegration|cluster'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') + = s_('ClusterIntegration|Learn more about %{link_to_documentation}').html_safe % { link_to_documentation: link } diff --git a/app/views/projects/clusters/login.html.haml b/app/views/projects/clusters/login.html.haml new file mode 100644 index 00000000000..ae132672b7e --- /dev/null +++ b/app/views/projects/clusters/login.html.haml @@ -0,0 +1,16 @@ +- breadcrumb_title "Cluster" +- page_title _("Login") + +.row.prepend-top-default + .col-sm-4 + = render 'sidebar' + .col-sm-8 + = render 'header' +.row + .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') + - 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 new file mode 100644 index 00000000000..c538d41ffad --- /dev/null +++ b/app/views/projects/clusters/new.html.haml @@ -0,0 +1,9 @@ +- 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 new file mode 100644 index 00000000000..aee6f904a62 --- /dev/null +++ b/app/views/projects/clusters/show.html.haml @@ -0,0 +1,70 @@ +- 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, + 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.') + - else + = s_('ClusterIntegration|Cluster integration is enabled for this project.') + - else + = s_('ClusterIntegration|Cluster integration is disabled for this project.') + + = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field| + = form_errors(@cluster) + .form-group.append-bottom-20 + %label.append-bottom-10 + = field.hidden_field :enabled, { class: 'js-toggle-input'} + + %button{ type: 'button', + class: "js-toggle-cluster project-feature-toggle #{'checked' unless !@cluster.enabled?} #{'disabled' unless can?(current_user, :update_cluster, @cluster)}", + 'aria-label': s_('ClusterIntegration|Toggle Cluster'), + disabled: !can?(current_user, :update_cluster, @cluster), + data: { 'enabled-text': 'Enabled', 'disabled-text': 'Disabled' } } + + - if can?(current_user, :update_cluster, @cluster) + .form-group + = field.submit s_('ClusterIntegration|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 } + + .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 + + .hidden.js-cluster-creating.alert.alert-info.alert-block{ role: 'alert' } + = s_('ClusterIntegration|Cluster is being created on Google Container Engine...') + + .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: '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"}) diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index c06e9f323af..71d30da14a9 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -6,18 +6,9 @@ #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } } - if @cycle_analytics_no_data - .landing.content-block{ "v-if" => "!isOverviewDialogDismissed" } - %button.dismiss-button{ type: 'button', 'aria-label': 'Dismiss Cycle Analytics introduction box', "@click" => "dismissOverviewDialog()" } - = icon("times") - .svg-container - = custom_icon('icon_cycle_analytics_splash') - .inner-content - %h4 - {{ __('Introducing Cycle Analytics') }} - %p - {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }} - %p - = link_to _('Read more'), help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' + %banner{ "v-if" => "!isOverviewDialogDismissed", + "documentation-link": help_page_path('user/project/cycle_analytics'), + "v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" } = icon("spinner spin", "v-show" => "isLoading") .wrapper{ "v-show" => "!isLoading && !hasError" } .panel.panel-default diff --git a/app/views/projects/diffs/_image_diff_frame.html.haml b/app/views/projects/diffs/_image_diff_frame.html.haml new file mode 100644 index 00000000000..dae73e10460 --- /dev/null +++ b/app/views/projects/diffs/_image_diff_frame.html.haml @@ -0,0 +1,5 @@ +- class_name = local_assigns.fetch(:class_name, '') +- note_type = local_assigns.fetch(:note_type, '') + +.frame{ class: class_name, data: { position: position, note_type: note_type } } + = image_tag(image_path, alt: alt, draggable: false, lazy: false) diff --git a/app/views/projects/diffs/_replaced_image_diff.html.haml b/app/views/projects/diffs/_replaced_image_diff.html.haml new file mode 100644 index 00000000000..8fc232b464e --- /dev/null +++ b/app/views/projects/diffs/_replaced_image_diff.html.haml @@ -0,0 +1,61 @@ +- blob = diff_file.blob +- old_blob = diff_file.old_blob +- blob_raw_path = diff_file_blob_raw_path(diff_file) +- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file) +- click_to_comment = local_assigns.fetch(:click_to_comment, true) +- diff_view_data = local_assigns.fetch(:diff_view_data, '') +- class_name = '' + +- if click_to_comment + - class_name = 'js-add-image-diff-note-button click-to-comment' + +.image.js-replaced-image{ data: diff_view_data } + .two-up.view + .wrap + .frame.deleted + = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false) + %p.image-info.hide + %span.meta-filesize= number_to_human_size(old_blob.size) + | + %strong W: + %span.meta-width + | + %strong H: + %span.meta-height + .wrap + = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path } + %p.image-info.hide + %span.meta-filesize= number_to_human_size(blob.size) + | + %strong W: + %span.meta-width + | + %strong H: + %span.meta-height + + .swipe.view.hide + .swipe-frame + .frame.deleted + = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false) + .swipe-wrap + = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path } + %span.swipe-bar + %span.top-handle + %span.bottom-handle + + .onion-skin.view.hide + .onion-skin-frame + .frame.deleted + = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false) + = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path } + .controls + .transparent + .drag-track + .dragger{ :style => "left: 0px;" } + .opaque + +.view-modes.hide + %ul.view-modes-menu + %li.two-up{ data: { mode: 'two-up' } } 2-up + %li.swipe{ data: { mode: 'swipe' } } Swipe + %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin diff --git a/app/views/projects/diffs/_single_image_diff.html.haml b/app/views/projects/diffs/_single_image_diff.html.haml new file mode 100644 index 00000000000..6b0c6bbe48f --- /dev/null +++ b/app/views/projects/diffs/_single_image_diff.html.haml @@ -0,0 +1,16 @@ +- blob = diff_file.blob +- old_blob = diff_file.old_blob +- blob_raw_path = diff_file_blob_raw_path(diff_file) +- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file) +- click_to_comment = local_assigns.fetch(:click_to_comment, true) +- diff_view_data = local_assigns.fetch(:diff_view_data, '') +- class_name = '' + +- if click_to_comment + - class_name = 'js-add-image-diff-note-button click-to-comment' + +.image.js-single-image{ data: diff_view_data } + .wrap + - single_class_name = diff_file.deleted_file? ? 'deleted' : 'added' + = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "#{single_class_name} #{class_name} js-image-frame", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.file_path } + %p.image-info= number_to_human_size(blob.size) diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml index 6b5233833c6..f190073c2fc 100644 --- a/app/views/projects/diffs/viewers/_image.html.haml +++ b/app/views/projects/diffs/viewers/_image.html.haml @@ -1,67 +1,13 @@ - diff_file = viewer.diff_file -- blob = diff_file.blob -- old_blob = diff_file.old_blob -- blob_raw_path = diff_file_blob_raw_path(diff_file) -- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file) +- image_point = Gitlab::Diff::ImagePoint.new(nil, nil, nil, nil) +- discussions = @grouped_diff_discussions[diff_file.new_path] if @grouped_diff_discussions + +- locals = { diff_file: diff_file, position: diff_file.position(image_point, position_type: :image).to_json, click_to_comment: true, diff_view_data: diff_view_data } - if diff_file.new_file? || diff_file.deleted_file? - .image - %span.wrap - .frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') } - = image_tag(blob_raw_path, alt: diff_file.file_path) - %p.image-info= number_to_human_size(blob.size) + = render partial: "projects/diffs/single_image_diff", locals: locals - else - .image - .two-up.view - %span.wrap - .frame.deleted - = image_tag(old_blob_raw_path, alt: diff_file.old_path) - %p.image-info.hide - %span.meta-filesize= number_to_human_size(old_blob.size) - | - %b W: - %span.meta-width - | - %b H: - %span.meta-height - %span.wrap - .frame.added - = image_tag(blob_raw_path, alt: diff_file.new_path) - %p.image-info.hide - %span.meta-filesize= number_to_human_size(blob.size) - | - %b W: - %span.meta-width - | - %b H: - %span.meta-height - - .swipe.view.hide - .swipe-frame - .frame.deleted - = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false) - .swipe-wrap - .frame.added - = image_tag(blob_raw_path, alt: diff_file.new_path, lazy: false) - %span.swipe-bar - %span.top-handle - %span.bottom-handle - - .onion-skin.view.hide - .onion-skin-frame - .frame.deleted - = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false) - .frame.added - = image_tag(blob_raw_path, alt: diff_file.new_path, lazy: false) - .controls - .transparent - .drag-track - .dragger{ :style => "left: 0px;" } - .opaque - + = render partial: "projects/diffs/replaced_image_diff", locals: locals - .view-modes.hide - %ul.view-modes-menu - %li.two-up{ data: { mode: 'two-up' } } 2-up - %li.swipe{ data: { mode: 'swipe' } } Swipe - %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin +.note-container + = render partial: "discussions/notes", collection: discussions, as: :discussion diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 8ae4fd94146..893e536e289 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -97,7 +97,7 @@ %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' %p - Perform advanced options such as housekeeping, exporting, archiving, renaming, transferring, or removing your project. + Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project. .settings-content.no-animate{ class: ('expanded' if expanded) } .sub-section %h4 Housekeeping diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index fbaf88356bf..b9fec8af4d7 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -27,7 +27,9 @@ .issuable-meta - if @issue.confidential - = icon('eye-slash', class: 'is-confidential') + = icon('eye-slash', class: 'issuable-warning-icon') + - if @issue.discussion_locked? + = icon('lock', class: 'issuable-warning-icon') = issuable_meta(@issue, @project, "Issue") .issuable-actions.js-issuable-actions diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index 43e23bb2200..d5c6d329ce4 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -48,7 +48,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| diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml index 4a238b99b58..9963cc93633 100644 --- a/app/views/projects/jobs/index.html.haml +++ b/app/views/projects/jobs/index.html.haml @@ -8,7 +8,7 @@ .nav-controls - if can?(current_user, :update_build, @project) - - if @all_builds.running_or_pending.any? + - if @all_builds.running_or_pending.limit(1).any? = link_to 'Cancel running', cancel_all_project_jobs_path(@project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 9ff85c2ee4c..cb723fe6a18 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -15,6 +15,8 @@ = icon('angle-double-left') .issuable-meta + - if @merge_request.discussion_locked? + = icon('lock', class: 'issuable-warning-icon') = issuable_meta(@merge_request, @project, "Merge request") .issuable-actions.js-issuable-actions diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index d3742f3e4be..d88e3d794d3 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -83,7 +83,7 @@ #pipelines.pipelines.tab-pane - if @pipelines.any? = render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request) - #diffs.diffs.tab-pane + #diffs.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked? } } -# This tab is always loaded via AJAX .mr-loading-status diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index fda068f08c2..7062c5b765e 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -1,5 +1,5 @@ - @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout -- add_to_breadcrumbs "Snippets", dashboard_snippets_path +- add_to_breadcrumbs "Snippets", project_snippets_path(@project) - breadcrumb_title @snippet.to_reference - page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets" diff --git a/app/views/projects/tree/_old_tree_content.html.haml b/app/views/projects/tree/_old_tree_content.html.haml index 820b947804e..6ea78851b8d 100644 --- a/app/views/projects/tree/_old_tree_content.html.haml +++ b/app/views/projects/tree/_old_tree_content.html.haml @@ -6,7 +6,7 @@ %th= s_('ProjectFileTree|Name') %th.hidden-xs .pull-left= _('Last commit') - %th.text-right= _('Last Update') + %th.text-right= _('Last update') - if @path.present? %tr.tree-item %td.tree-item-file-name 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/profiles/gpg_keys/_email_with_badge.html.haml b/app/views/shared/_email_with_badge.html.haml index 5f7844584e1..b7bbc109238 100644 --- a/app/views/profiles/gpg_keys/_email_with_badge.html.haml +++ b/app/views/shared/_email_with_badge.html.haml @@ -2,7 +2,7 @@ - css_classes << (verified ? 'verified': 'unverified') - text = verified ? 'Verified' : 'Unverified' -.gpg-email-badge - .gpg-email-badge-email= email +.email-badge + .email-badge-email= email %div{ class: css_classes } = text diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml index 3baa956b910..639f28cc210 100644 --- a/app/views/shared/builds/_tabs.html.haml +++ b/app/views/shared/builds/_tabs.html.haml @@ -3,22 +3,22 @@ = link_to build_path_proc.call(nil) do All %span.badge.js-totalbuilds-count - = number_with_delimiter(all_builds.count(:id)) + = limited_counter_with_delimiter(all_builds) %li{ class: active_when(scope == 'pending') }> = link_to build_path_proc.call('pending') do Pending %span.badge - = number_with_delimiter(all_builds.pending.count(:id)) + = limited_counter_with_delimiter(all_builds.pending) %li{ class: active_when(scope == 'running') }> = link_to build_path_proc.call('running') do Running %span.badge - = number_with_delimiter(all_builds.running.count(:id)) + = limited_counter_with_delimiter(all_builds.running) %li{ class: active_when(scope == 'finished') }> = link_to build_path_proc.call('finished') do Finished %span.badge - = number_with_delimiter(all_builds.finished.count(:id)) + = limited_counter_with_delimiter(all_builds.finished) diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index eb41daa0988..059dd24be6d 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -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/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 674f13ddb23..7b7411b1e23 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -119,6 +119,10 @@ %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe #js-confidential-entry-point + - if issuable.has_attribute?(:discussion_locked) + %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) - if current_user - subscribed = issuable.subscribed?(current_user, @project) 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/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index 725bf916592..71c0d740bc8 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -24,20 +24,21 @@ -# DiffNote = f.hidden_field :position - = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do - = render 'projects/zen', f: f, - attr: :note, - classes: 'note-textarea js-note-text', - placeholder: "Write a comment or drag your files here...", - supports_quick_actions: supports_quick_actions, - supports_autocomplete: supports_autocomplete - = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions - .error-alert - - .note-form-actions.clearfix - = render partial: 'shared/notes/comment_button' - - = yield(:note_actions) - - %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } - Discard draft + .discussion-form-container + = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do + = render 'projects/zen', f: f, + attr: :note, + classes: 'note-textarea js-note-text', + placeholder: "Write a comment or drag your files here...", + supports_quick_actions: supports_quick_actions, + supports_autocomplete: supports_autocomplete + = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions + .error-alert + + .note-form-actions.clearfix + = render partial: 'shared/notes/comment_button' + + = yield(:note_actions) + + %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } + Discard draft diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index 4f00a9f2759..b6085fd3af0 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -1,7 +1,10 @@ - return unless note.author - return if note.cross_reference_not_visible_for?(current_user) +- show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false) - note_editable = note_editable?(note) +- note_counter = local_assigns.fetch(:note_counter, 0) + %li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: { author_id: note.author.id, @@ -12,8 +15,18 @@ - if note.system = icon_for_system_note(note) - else - %a{ href: user_path(note.author) } + %a.image-diff-avatar-link{ href: user_path(note.author) } = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' + - if note.is_a?(DiffNote) && note.on_image? + - if show_image_comment_badge && note_counter == 0 + -# Only show this for the first comment in the discussion + %span.image-comment-badge.inverted + = icon('comment-o') + - elsif note_counter == 0 + - counter = badge_counter if local_assigns[:badge_counter] + - badge_class = "hidden" if @fresh_discussion || counter.nil? + %span.badge{ class: badge_class } + = counter .timeline-content .note-header .note-header-info diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml index e3e86709b8f..c6e18108c7a 100644 --- a/app/views/shared/notes/_notes_with_form.html.haml +++ b/app/views/shared/notes/_notes_with_form.html.haml @@ -1,3 +1,6 @@ +- issuable = @issue || @merge_request +- discussion_locked = issuable&.discussion_locked? + %ul#notes-list.notes.main-notes-list.timeline = render "shared/notes/notes" @@ -21,5 +24,14 @@ or = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link' to comment - +- elsif discussion_locked + .disabled-comment.text-center.prepend-top-default + %span.issuable-note-warning + %span.icon= sprite_icon('lock', size: 14) + %span + This + = issuable.class.to_s.titleize.downcase + is locked. Only + %b project members + can comment. %script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe 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 d0ffcc88d43..6c3cd6ecefe 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -9,7 +9,7 @@ = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") .user-profile - .cover-block.user-cover-block.layout-nav + .cover-block.user-cover-block.top-area .cover-controls - if @user == current_user = link_to profile_path, class: 'btn btn-gray has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index e2a1b3dcc41..52e7d346e74 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -6,6 +6,7 @@ class BuildFinishedWorker def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| + BuildTraceSectionsWorker.perform_async(build.id) BuildCoverageWorker.new.perform(build.id) BuildHooksWorker.new.perform(build.id) end diff --git a/app/workers/build_trace_sections_worker.rb b/app/workers/build_trace_sections_worker.rb new file mode 100644 index 00000000000..8c57e8f767b --- /dev/null +++ b/app/workers/build_trace_sections_worker.rb @@ -0,0 +1,8 @@ +class BuildTraceSectionsWorker + include Sidekiq::Worker + include PipelineQueue + + def perform(build_id) + Ci::Build.find_by(id: build_id)&.parse_trace_sections! + end +end diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb new file mode 100644 index 00000000000..63300b58a25 --- /dev/null +++ b/app/workers/cluster_provision_worker.rb @@ -0,0 +1,10 @@ +class ClusterProvisionWorker + include Sidekiq::Worker + include ClusterQueue + + def perform(cluster_id) + Gcp::Cluster.find_by_id(cluster_id).try do |cluster| + Ci::ProvisionClusterService.new.execute(cluster) + end + end +end diff --git a/app/workers/concerns/cluster_queue.rb b/app/workers/concerns/cluster_queue.rb new file mode 100644 index 00000000000..a5074d13220 --- /dev/null +++ b/app/workers/concerns/cluster_queue.rb @@ -0,0 +1,10 @@ +## +# Concern for setting Sidekiq settings for the various Gcp clusters workers. +# +module ClusterQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :gcp_cluster + 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/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb new file mode 100644 index 00000000000..5aa3bbdaa9d --- /dev/null +++ b/app/workers/wait_for_cluster_creation_worker.rb @@ -0,0 +1,27 @@ +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 + end + end + end +end |