From c8a4d202c99c772822a2b9b09fa6da2c90b2ae81 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 26 Sep 2017 09:51:24 +0200 Subject: [PATCH 01/35] favicon uploader generating ci status favicons --- Gemfile | 1 + Gemfile.lock | 2 + .../overlays/favicon_status_canceled.png | Bin 0 -> 864 bytes .../overlays/favicon_status_created.png | Bin 0 -> 889 bytes .../overlays/favicon_status_failed.png | Bin 0 -> 1015 bytes .../overlays/favicon_status_manual.png | Bin 0 -> 1067 bytes .../overlays/favicon_status_not_found.png | Bin 0 -> 945 bytes .../overlays/favicon_status_pending.png | Bin 0 -> 919 bytes .../overlays/favicon_status_running.png | Bin 0 -> 1077 bytes .../overlays/favicon_status_skipped.png | Bin 0 -> 923 bytes .../overlays/favicon_status_success.png | Bin 0 -> 1044 bytes .../overlays/favicon_status_warning.png | Bin 0 -> 830 bytes app/uploaders/favicon_uploader.rb | 44 ++++++++++++++++++ spec/uploaders/favicon_uploader_spec.rb | 38 +++++++++++++++ 14 files changed, 85 insertions(+) create mode 100644 app/assets/images/ci_favicons/overlays/favicon_status_canceled.png create mode 100644 app/assets/images/ci_favicons/overlays/favicon_status_created.png create mode 100644 app/assets/images/ci_favicons/overlays/favicon_status_failed.png create mode 100644 app/assets/images/ci_favicons/overlays/favicon_status_manual.png create mode 100644 app/assets/images/ci_favicons/overlays/favicon_status_not_found.png create mode 100644 app/assets/images/ci_favicons/overlays/favicon_status_pending.png create mode 100644 app/assets/images/ci_favicons/overlays/favicon_status_running.png create mode 100644 app/assets/images/ci_favicons/overlays/favicon_status_skipped.png create mode 100644 app/assets/images/ci_favicons/overlays/favicon_status_success.png create mode 100644 app/assets/images/ci_favicons/overlays/favicon_status_warning.png create mode 100644 app/uploaders/favicon_uploader.rb create mode 100644 spec/uploaders/favicon_uploader_spec.rb diff --git a/Gemfile b/Gemfile index 90fa659fe78..b508fb17742 100644 --- a/Gemfile +++ b/Gemfile @@ -104,6 +104,7 @@ gem 'hamlit', '~> 2.6.1' # Files attachments gem 'carrierwave', '~> 1.2' +gem 'mini_magick' # Drag and Drop UI gem 'dropzonejs-rails', '~> 0.7.1' diff --git a/Gemfile.lock b/Gemfile.lock index 2daaa3b516e..7437b6e7898 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -494,6 +494,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mimemagic (0.3.0) + mini_magick (4.8.0) mini_mime (1.0.0) mini_portile2 (2.3.0) minitest (5.7.0) @@ -1078,6 +1079,7 @@ DEPENDENCIES loofah (~> 2.2) mail_room (~> 0.9.1) method_source (~> 0.8) + mini_magick minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) mysql2 (~> 0.4.10) diff --git a/app/assets/images/ci_favicons/overlays/favicon_status_canceled.png b/app/assets/images/ci_favicons/overlays/favicon_status_canceled.png new file mode 100644 index 0000000000000000000000000000000000000000..8adaa9c600bb4ee553c874000c3a448d44109521 GIT binary patch literal 864 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/ci_favicons/overlays/favicon_status_not_found.png b/app/assets/images/ci_favicons/overlays/favicon_status_not_found.png new file mode 100644 index 0000000000000000000000000000000000000000..df3049315a9be0e0271f4926450cc211348e40b6 GIT binary patch literal 945 zcmV;i15W&jP)Px&Ye_^wR9Fe^R!d78Q4~It@tM>h2BO4;#3#Xp76wd{AJC1(Z8u%?C)jRVD7Y)7 zYuD{g?5_3~_^5?0iim;+u_TxXh6GWg$(VlMG$ZqH?4)+r9Jo&IJ@=mP-0z%wjv_5- zG0hSY2I>sj8Z+*XuEiP#_SDZ_NwmUJd|A+1S{4itjuC zG|rts054%}3C=AflgX@0?_BUZ-`w1c*J`yTfNEV`U3xScReO7TrL(hB047&gS8{fC zCdFb=PEJm)Ayu&QDwRsTvtqbBwko}TudS^;f^G!{!|`}r?eFjR+G?y6g?u zxNuNLMnQlNdRjuEkYK+Y9Ua*m0R1vFG^9e8KrTIQvuW>`@SQ`gA=_PH3x~r7-30CF=`pNu zjgFBWr$izVI?IV3ASy)NYf3fq@(FQmG_`Lc!P;3

5r6I_cFH#c04D>t zX15iAamTwimY9gKm~qUy#rVy70U7k|05(713C7t=`20+8hGSx4LfYHgZ{1EixKz-0 z2#`ERHZj@!$mr;(SYL|s4DG;Xp&17Ujxo?iF|QOfHur{WUoG$+rVMjKo=B zS7T!_jak2DMcm%rmh z@ty!cMOS1qdBMiQ+RpN7z5wePx&QAtEWR9Fe^R!vA$Q4~J+-5F={2V2y}FsNCWLbOP*ABu{iMWG-BrHg*(msYh_ zDB48@fn#H3V35R+1R6#J*&@jFDA^}`CpcED;d-!tCuuF_{@5D+U_u}`qF zIT%eBX*F zg-S*mi4CE6P%RTpZq%FeZewsCcO1-WX#8^u-nLrDu@7mY+P6e+0}=>zUAFpl^sHR6 zgg@^U{qszNn{Y(WDFcLl&9j$+W>f>ZKktBY)XsMVfR!I{1lndK6CmzX2#kOKT*Yu%k;$m)K`rVSfH7E9WTID_Xh~vYOqD5JBxa$SYf_u~RW&03#pje_p&RVk z=F{D5`GU2G3TN;xm!TvM|s_V z2E{(-2Q8NJ$#KBJHl4e;&+#iDA2d>Fs>x?(lg(~%?C3H2f`Y5_7+@!+!WjXedCrhE z%5M*u>}zxsZ;nws45GVM5rsC)3%%#&>}B#mBbBBa4HgtL=9^8wj`X2^t3+4xWC8>3 zb!NPbGGHF!%W4j*`z!L@&E#PD)D9q~&F#5s>?6=S7ady>rp6+i<7Rd;(Qg`MRD|Xs zbET1r5-!qUA#Nvn1;ubcB%}JAS`A}ET(zhACd2@xogIvin&)X<6aaESUpF+>Xs{5s t6TK3{5y?=Wt8c002ovPDHLkV1oKtr9c1x literal 0 HcmV?d00001 diff --git a/app/assets/images/ci_favicons/overlays/favicon_status_running.png b/app/assets/images/ci_favicons/overlays/favicon_status_running.png new file mode 100644 index 0000000000000000000000000000000000000000..ff4167c4b20a2a95106a92921c78fbd94308874f GIT binary patch literal 1077 zcmV-51j_q~P)Px&?@2^KR9Fe^Rb6ZpRTw>YW_P-EyIV_zlD47*DS;5wu!$mQfEM+E1QVl@7-Rf- z5G5rFzVQGt;h_NySs#q?!6+uAU^FIBg91v7@h8z}5Mc|3r7g?0Wm~#!8MiZYxu@G5 zCR;mBi*NROn9R(*-~G=0e$D}AZ8ih58JNw$|2+fZze>P{S$fuT@GZDL-~fO zz8_7VU1%3XyCY?yp~kRVmd5qOT~XA247-2pB+S{Axp`U$#BfF7;^vV7Ig3So5Hk%9_}5 zs0%};%D=LZiumH{T3uaaX@6JhvA_U0=Nz>Ss@we0N4kR`O z20)VGoUc@)HWT~iTpsT>pCwMW&<_^ z1^@?D^d`yp+3B9^NPC*jyVo2m_5uGz!a^M>ZVP-Jp&=ucKkHdlF?swr;~#$CkApwu z)aZF;&x2roJnViAjM$1lWHvYeB`owNy_Sz4$868h49@r7OtBkj;$==Wgk|-W?(fcC z({`lj^v|ce9iXaFSjg>O>o|Q4uN+AG2Iu~5Ep;wkcjXG`9w_J#I1(5DC6%g1VIjAd ztf?-7N>xukd*c2YJbcf13yp6(_;Zf*ZRvXG)4%}eNlB%uQCLa=7tO7}2hT6W+9ffR z69m(~ds72i*Uu|$$C-?YFaH=){^Gr_w=KHl58ZXU`Lgcq)SGaewr3xx1-ad81EV&` ze0kFb8;&*il2%qx^p*o@d&smiFrxPz-`aR}+5i}E{oZpQNyz6YEaZ05>%SRqzVq`Q z``UHOln|>=ZeQ{neWoqPbV|IO{DP9IZoBHMKl65VrLHyB7?VoCy==JmPxUEb>Xijs zURJ?dO2N54qYTiIR+LoPMqwehlU~ViBpJ>*M~&UihGWe|MRXU_Ui2{m)>sxe;<|w6wSpi00000NkvXXu0mjf8`chd literal 0 HcmV?d00001 diff --git a/app/assets/images/ci_favicons/overlays/favicon_status_skipped.png b/app/assets/images/ci_favicons/overlays/favicon_status_skipped.png new file mode 100644 index 0000000000000000000000000000000000000000..a9c36464b69a28c1af0634b03f4ce6dbc70b0c6a GIT binary patch literal 923 zcmV;M17!S(P)Px&RY^oaR9Fe^R!d78Q4~It@ez|MK|#d`8oNr{R7%<3k7$j zbZxioPP(=*E!#J&;9PX5oua8 z1I-LHGw{D>K>1e*SX^8jP*pWisZ<8=g#&>=K~a>Qcs%|Mzj{5K2N3{}va+)B48PL= z(Aaka0sI(a^B^~yNF;K1=G_Bc`{{H#rtA7VK($~nX!Q2>s*y-U+S}U&U~+SFBNrDJ za&&Yg#bWUmRz_A{CzHwdPK-N@os;$2OG`^nFt>n!U9nh9?d$8Sx6~Xjl}fU^yITPn zBt-cP5-;#;aMCvcYvxE4f@w0RADBO1^l2Q+xH3Hz^Seer_?WZpS)_eYjuNof~zea(sudnm$^R2C|Z|U#r278j%66;x*%AGQ)?3wVldmg>xGPm(SfPx&&PhZ;R9Fe^R#`|?VH7_9{cnzr<5H=WgL^&XWVSC8jR|21L808qo$ zIXlimEz{80m_@6!R=3#3J3U80Vs4xwj-W4{|JE5CM_3;P#Pra zLJ!Q#rPlZ~9=dN+pL$ziU(I=T*HK0It&3)WT)WjSBM|C|M$0=!4n(Nnk%nGRKR{PT zrHttuZH5H|!-m)ugz*|xw$-NcIvAYx+PnBoz#pxGHOI7Lyp3Wy=Bjm*tguT*7jFe?{HjrIjDO@}GFZy;AFJU!fi-?>fy{e*;CcI7&tq6?Tmm{8 zATx!n*9>5gWPsqPX%TcTJt%@#T@A3K>eO#4I$;u+Ba<-@vt|G)!*Me>=iYLzt{lqv zyt~Eiyt%Iv!UGI&a#^1H)6f&`5JHUN(8>5O2E2kK<0(iqh1F+46eyfnx*g_(C#aFT zotLETRnI(i@Lewk@@Zgt%>bA{nn7VI!x;=gL-tlg&4Kw-WF(S z1NBZv1}+wJlxZ{^KF@G6tQGs^@A7~H#Go8!$;&>!0H9^Q#U6)BrX<{I9Rod(Ha!*i zYWC^W{Q)CK=WYEx{UDOnO<9*3@tfZOP-(4Xx`ivXrmKGWjSmmyh`=x*A!G)(JJK9B z+@aUSP*8n_4MvX6OJ1TK6u+8qP-Xa6U8#&6#>Il%j`TYCA5ow>=QwKE?VRykSaTu< z6*XmD=oq%}mjKX^WRoavHC!yn?MSc4a7Z#Z=e$+!aPGW{4d?0D{r(I7Xomj>W@SqN O0000P)Px%_(?=TR9Fe^R!wLVQ4s##ZnbFq7cWLzq$*7hdg-C2p!A>>5qj{VplC&J(&AB6 z6!c>8C-Dbbico@h2t5?U9@K*utDp#?)IcnvsIh2lK}E&g9cSLUVz=44oAzdg>`vZ$ z^JeDFo0)Hc3RNOdi9jU+|9b?~Oa*{0VL2=`K`{qn%-=H~!8$xpExZyy({g}_&7`oC z--lU$BV_C+WsHNIEKHnI9e6Wo**_6>%qLz2TV+Aih(&ca>UW!{-2%+70cOR3uN;7n zPdEkxK)&B$Cw@ZFG1ZB)VQmD;(FJUvwDF0q#4LK&OXOHIC5`8a2DUl8kNC-u#G_o2 zlWXHwS6WmYp3?5bMTh9#5pjShjdNw$mpiAFdZQ9AL#p14<>sJK%OWx6XqU zN12y?p;SDI1M{?gX)-fGqK9a1RRmoHEh`$4vc7x z(q4l~Pz1JK*JA&CeRynhd(MZy0!B3mut;y8iXNQ-ZtVkv2hms#7<(b5baOI#1#RMa zZlgUg%mdh)Gq7-VX~{I7iCZRz7wB|K%iP`%4EI^wSL?z@KvXfLYE--1$rkumiqvnF z9{0->?u&8R-_6MQI+6n}p3`x>98?%O>R0WKTQfk@2|&1?KEDEPwAmb;x2VEN|8nTD zhEzc{0T%m5Dae^T_il)&XPL!d_JuBDy=N>^1iVd%qn%;wJf8LfeQRyxt^xM9Kg$#T zNW#gTXPXJ%iv<|e%#q|$aw$?0dYe2>8d6o41{Ue<(q8}mB9c;*Y*&iE38`Dct`4E- z?H4{R2SkDXx?xlkU|~Z^e&W*Mn2$=GbFL0@wE89CMBY!o0ECXKRt<{9 literal 0 HcmV?d00001 diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb new file mode 100644 index 00000000000..dc30e838337 --- /dev/null +++ b/app/uploaders/favicon_uploader.rb @@ -0,0 +1,44 @@ +class FaviconUploader < AttachmentUploader + include CarrierWave::MiniMagick + + STATUS_ICON_NAMES = [ + :status_not_found, + :status_canceled, + :status_success, + :status_skipped, + :status_created, + :status_failed, + :status_warning, + :status_pending, + :status_manual, + :status_running + ].freeze + + version :default_without_format_conversion do + process resize_to_fill: [32, 32] + end + + # this intermediate version generates an image in the ico format but with the + # original file suffix. + version :_default, from_version: :default_without_format_conversion do + process convert: 'ico' + end + + version :default, from_version: :_default + + STATUS_ICON_NAMES.each do |status_name| + version status_name, from_version: :default do + process status_favicon: status_name + end + end + + def status_favicon(status_name) + manipulate! do |img| + overlay_path = Rails.root.join("app/assets/images/ci_favicons/overlays/favicon_#{status_name}.png") + overlay = MiniMagick::Image.open(overlay_path) + img.composite(overlay) do |c| + c.compose 'over' + end + end + end +end diff --git a/spec/uploaders/favicon_uploader_spec.rb b/spec/uploaders/favicon_uploader_spec.rb new file mode 100644 index 00000000000..5989d294112 --- /dev/null +++ b/spec/uploaders/favicon_uploader_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +RSpec.describe FaviconUploader do + include CarrierWave::Test::Matchers + + let(:uploader) { described_class.new(build_stubbed(:user)) } + + after do + uploader.remove! + end + + def upload_fixture(filename) + fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) + end + + context 'versions' do + before do + uploader.store!(upload_fixture('dk.png')) + end + + it 'has the correct format' do + expect(uploader.default).to be_format('ico') + end + + it 'has the correct dimensions' do + expect(uploader.default).to have_dimensions(32, 32) + end + + it 'generates all the status icons' do + # make sure that the following each statement actually loops + expect(FaviconUploader::STATUS_ICON_NAMES.count).to eq 10 + + FaviconUploader::STATUS_ICON_NAMES.each do |status_name| + expect(File.exist?(uploader.status_not_found.file.file)).to be true + end + end + end +end From ce6172e863f982c73fedc9f4a73877543c30cb1c Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 26 Sep 2017 09:54:32 +0200 Subject: [PATCH 02/35] allow uploading favicon in appearance settings --- app/controllers/admin/appearances_controller.rb | 9 +++++++++ app/controllers/concerns/uploads_actions.rb | 2 +- app/models/appearance.rb | 1 + app/views/admin/appearances/_form.html.haml | 17 +++++++++++++++++ config/routes/admin.rb | 1 + config/routes/uploads.rb | 2 +- ...20170925184228_add_favicon_to_appearances.rb | 7 +++++++ db/schema.rb | 1 + 8 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20170925184228_add_favicon_to_appearances.rb diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb index ea302f17d16..9aaec905734 100644 --- a/app/controllers/admin/appearances_controller.rb +++ b/app/controllers/admin/appearances_controller.rb @@ -41,6 +41,13 @@ class Admin::AppearancesController < Admin::ApplicationController redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.' end + def favicon + @appearance.remove_favicon! + @appearance.save + + redirect_to admin_appearances_path, notice: 'Favicon was succesfully removed.' + end + private # Use callbacks to share common setup or constraints between actions. @@ -61,6 +68,8 @@ class Admin::AppearancesController < Admin::ApplicationController logo_cache header_logo header_logo_cache + favicon + favicon_cache new_project_guidelines updated_by ] diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index b9b9b6e4e88..80049044124 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -2,7 +2,7 @@ module UploadsActions include Gitlab::Utils::StrongMemoize include SendFileUpload - UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo).freeze + UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze def create link_to_file = UploadService.new(model, params[:file], uploader_class).execute diff --git a/app/models/appearance.rb b/app/models/appearance.rb index 67cc84a9140..b770aadef0e 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -14,6 +14,7 @@ class Appearance < ActiveRecord::Base mount_uploader :logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader + mount_uploader :favicon, FaviconUploader # Overrides CacheableAttributes.current_without_cache def self.current_without_cache diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 5e08837255f..163ce55eb53 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -55,6 +55,23 @@ .hint Guidelines parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}. + %fieldset.app_logo + %legend + Favicon: + .form-group + = f.label :favicon, 'Favicon', class: 'control-label' + .col-sm-10 + - if @appearance.favicon? + = image_tag @appearance.favicon.default_without_format_conversion.url, class: 'appearance-light-logo-preview' + - if @appearance.persisted? + %br + = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo" + %hr + = f.hidden_field :favicon_cache + = f.file_field :favicon, class: '' + .hint + Maximum file size is 1MB. The resulting favicons will be cropped to be square and scaled down to a size of 32x32 px. + .form-actions = f.submit 'Save', class: 'btn btn-save append-right-10' - if @appearance.persisted? diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 3cca1210e39..ff27ceb50dc 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -102,6 +102,7 @@ namespace :admin do get :preview_sign_in delete :logo delete :header_logos + delete :favicon end end diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb index 6370645bcb9..6becadd57ae 100644 --- a/config/routes/uploads.rb +++ b/config/routes/uploads.rb @@ -17,7 +17,7 @@ scope path: :uploads do # Appearance get "-/system/:model/:mounted_as/:id/:filename", to: "uploads#show", - constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ } + constraints: { model: /appearance/, mounted_as: /logo|header_logo|favicon/, filename: /.+/ } # Project markdown uploads get ":namespace_id/:project_id/:secret/:filename", diff --git a/db/migrate/20170925184228_add_favicon_to_appearances.rb b/db/migrate/20170925184228_add_favicon_to_appearances.rb new file mode 100644 index 00000000000..65083733afb --- /dev/null +++ b/db/migrate/20170925184228_add_favicon_to_appearances.rb @@ -0,0 +1,7 @@ +class AddFaviconToAppearances < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :appearances, :favicon, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index a6b0706b02a..89c8acba3e6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -38,6 +38,7 @@ ActiveRecord::Schema.define(version: 20180529152628) do t.integer "cached_markdown_version" t.text "new_project_guidelines" t.text "new_project_guidelines_html" + t.string "favicon" end create_table "application_setting_terms", force: :cascade do |t| From 18d4f121d347bbc91f76b8112f797076864c6b33 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 26 Sep 2017 15:02:00 +0200 Subject: [PATCH 03/35] fix carrierwave suffix for different format when versions have a different file format from the original file carrierwave constructs a wrong url (with the original file suffix). --- app/uploaders/favicon_uploader.rb | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb index dc30e838337..b48ef770461 100644 --- a/app/uploaders/favicon_uploader.rb +++ b/app/uploaders/favicon_uploader.rb @@ -14,24 +14,27 @@ class FaviconUploader < AttachmentUploader :status_running ].freeze - version :default_without_format_conversion do + version :default do process resize_to_fill: [32, 32] - end - - # this intermediate version generates an image in the ico format but with the - # original file suffix. - version :_default, from_version: :default_without_format_conversion do process convert: 'ico' - end - version :default, from_version: :_default + def full_filename(filename) + filename_for_different_format(super(filename), 'ico') + end + end STATUS_ICON_NAMES.each do |status_name| version status_name, from_version: :default do process status_favicon: status_name + + def full_filename(filename) + filename_for_different_format(super(filename), 'ico') + end end end + private + def status_favicon(status_name) manipulate! do |img| overlay_path = Rails.root.join("app/assets/images/ci_favicons/overlays/favicon_#{status_name}.png") @@ -41,4 +44,8 @@ class FaviconUploader < AttachmentUploader end end end + + def filename_for_different_format(filename, format) + filename.chomp(File.extname(filename)) + ".#{format}" + end end From ff24be48556c8a7d8e9a55fc667d0713b90ac591 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 26 Sep 2017 15:02:55 +0200 Subject: [PATCH 04/35] show avatar status versions on appearance page --- app/views/admin/appearances/_form.html.haml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 163ce55eb53..8d73bd8fb55 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -62,7 +62,13 @@ = f.label :favicon, 'Favicon', class: 'control-label' .col-sm-10 - if @appearance.favicon? - = image_tag @appearance.favicon.default_without_format_conversion.url, class: 'appearance-light-logo-preview' + = image_tag @appearance.favicon.default.url, class: 'appearance-light-logo-preview' + - if @appearance.favicon? + = f.label :favicon, 'Generated status icons', class: 'control-label' + .col-sm-10 + - if @appearance.favicon? + - FaviconUploader::STATUS_ICON_NAMES.each do |status_name| + = image_tag @appearance.favicon.public_send(status_name).url, class: 'appearance-light-logo-preview' - if @appearance.persisted? %br = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo" From 40d8d7df4bd437efc81f0bdff5f93b4b65844cb5 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 26 Sep 2017 16:07:53 +0200 Subject: [PATCH 05/35] feature spec for managing appearance > favicon --- spec/features/admin/admin_appearance_spec.rb | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb index d91dcf76191..556aa10d226 100644 --- a/spec/features/admin/admin_appearance_spec.rb +++ b/spec/features/admin/admin_appearance_spec.rb @@ -76,6 +76,40 @@ feature 'Admin Appearance' do expect(page).not_to have_css(header_logo_selector) end + scenario 'Favicon' do + sign_in(create(:admin)) + visit admin_appearances_path + + attach_file(:appearance_favicon, logo_fixture) + click_button 'Save' + + expect(page).to have_css('//img[data-src$="/default_dk.ico"]') + expect(page).to have_css('//img[data-src$="/status_canceled_dk.ico"]') + expect(page).to have_css('//img[data-src$="/status_created_dk.ico"]') + expect(page).to have_css('//img[data-src$="/status_failed_dk.ico"]') + expect(page).to have_css('//img[data-src$="/status_manual_dk.ico"]') + expect(page).to have_css('//img[data-src$="/status_not_found_dk.ico"]') + expect(page).to have_css('//img[data-src$="/status_pending_dk.ico"]') + expect(page).to have_css('//img[data-src$="/status_running_dk.ico"]') + expect(page).to have_css('//img[data-src$="/status_skipped_dk.ico"]') + expect(page).to have_css('//img[data-src$="/status_success_dk.ico"]') + expect(page).to have_css('//img[data-src$="/status_warning_dk.ico"]') + + click_link 'Remove favicon' + + expect(page).not_to have_css('//img[data-src$="/default_dk.ico"]') + expect(page).not_to have_css('//img[data-src$="/status_canceled_dk.ico"]') + expect(page).not_to have_css('//img[data-src$="/status_created_dk.ico"]') + expect(page).not_to have_css('//img[data-src$="/status_failed_dk.ico"]') + expect(page).not_to have_css('//img[data-src$="/status_manual_dk.ico"]') + expect(page).not_to have_css('//img[data-src$="/status_not_found_dk.ico"]') + expect(page).not_to have_css('//img[data-src$="/status_pending_dk.ico"]') + expect(page).not_to have_css('//img[data-src$="/status_running_dk.ico"]') + expect(page).not_to have_css('//img[data-src$="/status_skipped_dk.ico"]') + expect(page).not_to have_css('//img[data-src$="/status_success_dk.ico"]') + expect(page).not_to have_css('//img[data-src$="/status_warning_dk.ico"]') + end + def expect_custom_sign_in_appearance(appearance) expect(page).to have_content appearance.title expect(page).to have_content appearance.description From 8967fc0477d176cb5b93ad3a9f2cf19eaca14876 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 26 Sep 2017 16:22:04 +0200 Subject: [PATCH 06/35] use custom main favicon --- app/helpers/appearances_helper.rb | 4 ++++ app/helpers/page_layout_helper.rb | 1 + spec/helpers/page_layout_helper_spec.rb | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index f48db024e3f..52ee8aec4ad 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -33,4 +33,8 @@ module AppearancesHelper render 'shared/logo_type.svg' end end + + def brand_favicon + brand_item&.favicon + end end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index a8397b03d63..b9b0e08cde5 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -39,6 +39,7 @@ module PageLayoutHelper end def favicon + return brand_favicon.default.url if brand_favicon return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY']) return 'favicon-blue.ico' if Rails.env.development? diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index b77114a8152..53ecf25612f 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -55,6 +55,11 @@ describe PageLayoutHelper do stub_env('CANARY', 'true') expect(helper.favicon).to eq 'favicon-yellow.ico' end + + it 'uses the custom favicon if an favicon appearance is present' do + create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) + expect(helper.favicon).to match %r{/uploads/-/system/appearance/favicon/\d+/default_dk.ico} + end end describe 'page_image' do From 606b23dd39bc67b01a52bb189dd938fba87badd2 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 26 Sep 2017 16:23:18 +0200 Subject: [PATCH 07/35] sort status icon names by name --- app/uploaders/favicon_uploader.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb index b48ef770461..d64fa5b1609 100644 --- a/app/uploaders/favicon_uploader.rb +++ b/app/uploaders/favicon_uploader.rb @@ -2,16 +2,16 @@ class FaviconUploader < AttachmentUploader include CarrierWave::MiniMagick STATUS_ICON_NAMES = [ - :status_not_found, :status_canceled, - :status_success, - :status_skipped, :status_created, :status_failed, - :status_warning, - :status_pending, :status_manual, - :status_running + :status_not_found, + :status_pending, + :status_running, + :status_skipped, + :status_success, + :status_warning ].freeze version :default do From a6f3f6b8cd2e79acbc824c401435284635071e1a Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 26 Sep 2017 17:04:37 +0200 Subject: [PATCH 08/35] extract favicon logic to lib class --- app/helpers/appearances_helper.rb | 4 ---- app/helpers/page_layout_helper.rb | 6 +----- lib/gitlab/favicon.rb | 23 +++++++++++++++++++++++ spec/helpers/page_layout_helper_spec.rb | 22 ---------------------- spec/lib/gitlab/favicon_spec.rb | 25 +++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 31 deletions(-) create mode 100644 lib/gitlab/favicon.rb create mode 100644 spec/lib/gitlab/favicon_spec.rb diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 52ee8aec4ad..f48db024e3f 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -33,8 +33,4 @@ module AppearancesHelper render 'shared/logo_type.svg' end end - - def brand_favicon - brand_item&.favicon - end end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index b9b0e08cde5..c3dd204181d 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -39,11 +39,7 @@ module PageLayoutHelper end def favicon - return brand_favicon.default.url if brand_favicon - return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY']) - return 'favicon-blue.ico' if Rails.env.development? - - 'favicon.ico' + Gitlab::Favicon.default end def page_image diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb new file mode 100644 index 00000000000..27150f8d1ea --- /dev/null +++ b/lib/gitlab/favicon.rb @@ -0,0 +1,23 @@ +module Gitlab + class Favicon + class << self + def default + return appearance_favicon.default.url if appearance_favicon + return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY']) + return 'favicon-blue.ico' if Rails.env.development? + + 'favicon.ico' + end + + private + + def appearance + @appearance ||= Appearance.current + end + + def appearance_favicon + appearance&.favicon + end + end + end +end diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index 53ecf25612f..cf98eed27f1 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -40,28 +40,6 @@ describe PageLayoutHelper do end end - describe 'favicon' do - it 'defaults to favicon.ico' do - allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) - expect(helper.favicon).to eq 'favicon.ico' - end - - it 'has blue favicon for development' do - allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) - expect(helper.favicon).to eq 'favicon-blue.ico' - end - - it 'has yellow favicon for canary' do - stub_env('CANARY', 'true') - expect(helper.favicon).to eq 'favicon-yellow.ico' - end - - it 'uses the custom favicon if an favicon appearance is present' do - create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) - expect(helper.favicon).to match %r{/uploads/-/system/appearance/favicon/\d+/default_dk.ico} - end - end - describe 'page_image' do it 'defaults to the GitLab logo' do expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png' diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb new file mode 100644 index 00000000000..51a8ed47abf --- /dev/null +++ b/spec/lib/gitlab/favicon_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe Gitlab::Favicon do + describe '.default' do + it 'defaults to favicon.ico' do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) + expect(described_class.default).to eq 'favicon.ico' + end + + it 'has blue favicon for development' do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) + expect(described_class.default).to eq 'favicon-blue.ico' + end + + it 'has yellow favicon for canary' do + stub_env('CANARY', 'true') + expect(described_class.favicon).to eq 'favicon-yellow.ico' + end + + it 'uses the custom favicon if a favicon appearance is present' do + create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) + expect(described_class.default).to match %r{/uploads/-/system/appearance/favicon/\d+/default_dk.ico} + end + end +end From 44d7b1583348513f8faa680a864efdbb39be70ab Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 27 Sep 2017 13:18:49 +0200 Subject: [PATCH 09/35] use custom favicon for ci build status favicons --- app/serializers/status_entity.rb | 11 +---------- lib/gitlab/ci/status/canceled.rb | 2 +- lib/gitlab/ci/status/created.rb | 2 +- lib/gitlab/ci/status/failed.rb | 2 +- lib/gitlab/ci/status/manual.rb | 2 +- lib/gitlab/ci/status/pending.rb | 2 +- lib/gitlab/ci/status/running.rb | 2 +- lib/gitlab/ci/status/skipped.rb | 2 +- lib/gitlab/ci/status/success.rb | 2 +- lib/gitlab/favicon.rb | 18 +++++++++++++++--- spec/lib/gitlab/favicon_spec.rb | 13 +++++++++++++ 11 files changed, 37 insertions(+), 21 deletions(-) diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb index 8e8bda2f9df..f4d0a33d466 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/status_entity.rb @@ -7,16 +7,7 @@ class StatusEntity < Grape::Entity expose :details_path expose :favicon do |status| - dir = - if Gitlab::Utils.to_boolean(ENV['CANARY']) - File.join('ci_favicons', 'canary') - elsif Rails.env.development? - File.join('ci_favicons', 'dev') - else - 'ci_favicons' - end - - ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico")) + ActionController::Base.helpers.image_path(status.favicon) end expose :action, if: -> (status, _) { status.has_action? } do diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb index e6195a60d4f..0d71ff03163 100644 --- a/lib/gitlab/ci/status/canceled.rb +++ b/lib/gitlab/ci/status/canceled.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - 'favicon_status_canceled' + Gitlab::Favicon.status('canceled') end end end diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb index 846f00b83dd..de86191dfeb 100644 --- a/lib/gitlab/ci/status/created.rb +++ b/lib/gitlab/ci/status/created.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - 'favicon_status_created' + Gitlab::Favicon.status('created') end end end diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb index 27ce85bd3ed..20e2050108c 100644 --- a/lib/gitlab/ci/status/failed.rb +++ b/lib/gitlab/ci/status/failed.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - 'favicon_status_failed' + Gitlab::Favicon.status('failed') end end end diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb index fc387e2fd25..2c02ce6e870 100644 --- a/lib/gitlab/ci/status/manual.rb +++ b/lib/gitlab/ci/status/manual.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - 'favicon_status_manual' + Gitlab::Favicon.status('manual') end end end diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb index 6780780db32..9122d11cfed 100644 --- a/lib/gitlab/ci/status/pending.rb +++ b/lib/gitlab/ci/status/pending.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - 'favicon_status_pending' + Gitlab::Favicon.status('pending') end end end diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb index ee13905e46d..9bc48ec2c29 100644 --- a/lib/gitlab/ci/status/running.rb +++ b/lib/gitlab/ci/status/running.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - 'favicon_status_running' + Gitlab::Favicon.status('running') end end end diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb index 0dbdc4de426..b404118cd3b 100644 --- a/lib/gitlab/ci/status/skipped.rb +++ b/lib/gitlab/ci/status/skipped.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - 'favicon_status_skipped' + Gitlab::Favicon.status('skipped') end end end diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb index 731013ec017..be7e5d60b26 100644 --- a/lib/gitlab/ci/status/success.rb +++ b/lib/gitlab/ci/status/success.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - 'favicon_status_success' + Gitlab::Favicon.status('success') end end end diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb index 27150f8d1ea..17e737ac913 100644 --- a/lib/gitlab/favicon.rb +++ b/lib/gitlab/favicon.rb @@ -2,21 +2,33 @@ module Gitlab class Favicon class << self def default - return appearance_favicon.default.url if appearance_favicon + return appearance_favicon.default.url if appearance_favicon.exists? return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY']) return 'favicon-blue.ico' if Rails.env.development? 'favicon.ico' end + def status(status_name) + if appearance_favicon.exists? + appearance_favicon.public_send("status_#{status_name}").url + else + dir = 'ci_favicons' + dir = File.join(dir, 'dev') if Rails.env.development? + dir = File.join(dir, 'canary') if Gitlab::Utils.to_boolean(ENV['CANARY']) + + File.join(dir, "favicon_status_#{status_name}.ico") + end + end + private def appearance - @appearance ||= Appearance.current + Appearance.current || Appearance.new end def appearance_favicon - appearance&.favicon + appearance.favicon end end end diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb index 51a8ed47abf..d74dabf1458 100644 --- a/spec/lib/gitlab/favicon_spec.rb +++ b/spec/lib/gitlab/favicon_spec.rb @@ -22,4 +22,17 @@ RSpec.describe Gitlab::Favicon do expect(described_class.default).to match %r{/uploads/-/system/appearance/favicon/\d+/default_dk.ico} end end + + describe '.status' do + subject { described_class.status('created') } + + it 'defaults to the stock icon' do + expect(subject).to eq 'ci_favicons/favicon_status_created.ico' + end + + it 'uses the custom favicon if a favicon appearance is present' do + create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) + expect(subject).to match(%r{/uploads/-/system/appearance/favicon/\d+/status_created_dk.ico}) + end + end end From 0a76bcb412b6a193d30c930312ac40dc21e777e6 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 27 Sep 2017 13:21:31 +0200 Subject: [PATCH 10/35] register ico mime type `#send_file` won't properly set the favicon's content type otherwise. --- config/initializers/mime_types.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index e9326653cbe..acbdf8de5a6 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -15,3 +15,5 @@ Mime::Type.register "video/ogg", :ogv Mime::Type.unregister :json Mime::Type.register 'application/json', :json, [LfsRequest::CONTENT_TYPE, 'application/json'] + +Mime::Type.register 'image/x-icon', :ico From bf27c6841c1cb6b68f67d33d6eb2de63ad8b390f Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 27 Sep 2017 13:21:58 +0200 Subject: [PATCH 11/35] send ico files with inline disposition --- app/uploaders/uploader_helper.rb | 2 +- spec/models/group_spec.rb | 2 +- spec/models/project_spec.rb | 2 +- spec/models/user_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index fd446d31092..207928b61d0 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -1,6 +1,6 @@ # Extra methods for uploader module UploaderHelper - IMAGE_EXT = %w[png jpg jpeg gif bmp tiff].freeze + IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze # We recommend using the .mp4 format over .mov. Videos in .mov format can # still be used but you really need to make sure they are served with the # proper MIME type video/mp4 and not video/quicktime or your videos won't play diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index f83b52e8975..8f1f4938065 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -240,7 +240,7 @@ describe Group do it "is false if avatar is html page" do group.update_attribute(:avatar, 'uploads/avatar.html') - expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff"]) + expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico"]) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9a76452a808..ed1253c6d97 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -960,7 +960,7 @@ describe Project do it 'is false if avatar is html page' do project.update_attribute(:avatar, 'uploads/avatar.html') - expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff']) + expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico']) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 09dfeae6377..097144d04ce 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1260,7 +1260,7 @@ describe User do it 'is false if avatar is html page' do user.update_attribute(:avatar, 'uploads/avatar.html') - expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff']) + expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico']) end end From 822023c64ccab23cfdacb42e191dcec4f812adfd Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 27 Sep 2017 13:23:05 +0200 Subject: [PATCH 12/35] Add a '?' to the custom favicon's urls Without the '?' at the end of the favicon url the custom favicon (i.e. the favicons that are served through `UploadController`) are not shown in the browser. It may have something to do with how `#send_file` / `#send_data` set http headers. When serving the same icon file from the public directory everything is fine. --- lib/gitlab/favicon.rb | 11 +++++++++-- spec/lib/gitlab/favicon_spec.rb | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb index 17e737ac913..e7fe0bff5b8 100644 --- a/lib/gitlab/favicon.rb +++ b/lib/gitlab/favicon.rb @@ -2,7 +2,7 @@ module Gitlab class Favicon class << self def default - return appearance_favicon.default.url if appearance_favicon.exists? + return custom_favicon_url(appearance_favicon.default.url) if appearance_favicon.exists? return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY']) return 'favicon-blue.ico' if Rails.env.development? @@ -11,7 +11,7 @@ module Gitlab def status(status_name) if appearance_favicon.exists? - appearance_favicon.public_send("status_#{status_name}").url + custom_favicon_url(appearance_favicon.public_send("status_#{status_name}").url) # rubocop:disable GitlabSecurity/PublicSend else dir = 'ci_favicons' dir = File.join(dir, 'dev') if Rails.env.development? @@ -30,6 +30,13 @@ module Gitlab def appearance_favicon appearance.favicon end + + # Without the '?' at the end of the favicon url the custom favicon (i.e. + # the favicons that are served through `UploadController`) are not shown + # in the browser. + def custom_favicon_url(url) + "#{url}?" + end end end end diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb index d74dabf1458..fe0bff51308 100644 --- a/spec/lib/gitlab/favicon_spec.rb +++ b/spec/lib/gitlab/favicon_spec.rb @@ -19,7 +19,7 @@ RSpec.describe Gitlab::Favicon do it 'uses the custom favicon if a favicon appearance is present' do create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) - expect(described_class.default).to match %r{/uploads/-/system/appearance/favicon/\d+/default_dk.ico} + expect(described_class.default).to match %r{/uploads/-/system/appearance/favicon/\d+/default_dk.ico\?} end end @@ -32,7 +32,7 @@ RSpec.describe Gitlab::Favicon do it 'uses the custom favicon if a favicon appearance is present' do create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) - expect(subject).to match(%r{/uploads/-/system/appearance/favicon/\d+/status_created_dk.ico}) + expect(subject).to match(%r{/uploads/-/system/appearance/favicon/\d+/status_created_dk.ico\?}) end end end From 85a8e6f26a8fa0ea9f430f0094fb14706bfd2991 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 27 Sep 2017 15:03:49 +0200 Subject: [PATCH 13/35] whitelist allowed file types for custom favicons --- app/uploaders/favicon_uploader.rb | 4 ++++ config/locales/carrierwave.en.yml | 14 ++++++++++++++ spec/features/admin/admin_appearance_spec.rb | 6 ++++++ 3 files changed, 24 insertions(+) create mode 100644 config/locales/carrierwave.en.yml diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb index d64fa5b1609..d3debc16fa9 100644 --- a/app/uploaders/favicon_uploader.rb +++ b/app/uploaders/favicon_uploader.rb @@ -33,6 +33,10 @@ class FaviconUploader < AttachmentUploader end end + def extension_whitelist + UploaderHelper::IMAGE_EXT + end + private def status_favicon(status_name) diff --git a/config/locales/carrierwave.en.yml b/config/locales/carrierwave.en.yml new file mode 100644 index 00000000000..12619226460 --- /dev/null +++ b/config/locales/carrierwave.en.yml @@ -0,0 +1,14 @@ +en: + errors: + messages: + carrierwave_processing_error: failed to be processed + carrierwave_integrity_error: is not of an allowed file type + carrierwave_download_error: could not be downloaded + extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}" + extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}" + content_type_whitelist_error: "You are not allowed to upload %{content_type} files" + content_type_blacklist_error: "You are not allowed to upload %{content_type} files" + rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image?" + mini_magick_processing_error: "Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: %{e}" + min_size_error: "File size should be greater than %{min_size}" + max_size_error: "File size should be less than %{max_size}" diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb index 556aa10d226..ffffd14752e 100644 --- a/spec/features/admin/admin_appearance_spec.rb +++ b/spec/features/admin/admin_appearance_spec.rb @@ -108,6 +108,12 @@ feature 'Admin Appearance' do expect(page).not_to have_css('//img[data-src$="/status_skipped_dk.ico"]') expect(page).not_to have_css('//img[data-src$="/status_success_dk.ico"]') expect(page).not_to have_css('//img[data-src$="/status_warning_dk.ico"]') + + # allowed file types + attach_file(:appearance_favicon, Rails.root.join('spec', 'fixtures', 'sanitized.svg')) + click_button 'Save' + + expect(page).to have_content 'Favicon You are not allowed to upload "svg" files, allowed types: png, jpg, jpeg, gif, bmp, tiff, ico' end def expect_custom_sign_in_appearance(appearance) From 40ffa8401b96dda5f67ea699dbcca0ff64263810 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 28 Sep 2017 10:45:50 +0200 Subject: [PATCH 14/35] add request store caching to favicon --- lib/gitlab/favicon.rb | 2 +- spec/lib/gitlab/favicon_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb index e7fe0bff5b8..8802f58e31c 100644 --- a/lib/gitlab/favicon.rb +++ b/lib/gitlab/favicon.rb @@ -24,7 +24,7 @@ module Gitlab private def appearance - Appearance.current || Appearance.new + RequestStore.store[:appearance] ||= (Appearance.current || Appearance.new) end def appearance_favicon diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb index fe0bff51308..ddfa81c0b5d 100644 --- a/spec/lib/gitlab/favicon_spec.rb +++ b/spec/lib/gitlab/favicon_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Gitlab::Favicon do +RSpec.describe Gitlab::Favicon, :request_store do describe '.default' do it 'defaults to favicon.ico' do allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) From 67fe0a17d87a7a5380b41e04ef23212d5da637ba Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 28 Sep 2017 13:57:08 +0200 Subject: [PATCH 15/35] call Gitlab::Favicon.status in serializer this ways we can keep the `lib/gitlab/ci/status/*` classes to return the bare favicon name as it was before. also the favicon uploader versions are now have the same names as the stock favicons (+ `favicon_` prefix), which makes working with the status names easier. --- app/helpers/page_layout_helper.rb | 2 +- app/serializers/status_entity.rb | 2 +- app/uploaders/favicon_uploader.rb | 26 ++++++++++----------- app/views/admin/appearances/_form.html.haml | 2 +- lib/gitlab/ci/status/canceled.rb | 2 +- lib/gitlab/ci/status/created.rb | 2 +- lib/gitlab/ci/status/failed.rb | 2 +- lib/gitlab/ci/status/manual.rb | 2 +- lib/gitlab/ci/status/pending.rb | 2 +- lib/gitlab/ci/status/running.rb | 2 +- lib/gitlab/ci/status/skipped.rb | 2 +- lib/gitlab/ci/status/success.rb | 2 +- lib/gitlab/favicon.rb | 17 ++++++++------ spec/lib/gitlab/favicon_spec.rb | 16 ++++++------- spec/uploaders/favicon_uploader_spec.rb | 6 ++--- 15 files changed, 45 insertions(+), 42 deletions(-) diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index c3dd204181d..68d892393ef 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -39,7 +39,7 @@ module PageLayoutHelper end def favicon - Gitlab::Favicon.default + Gitlab::Favicon.main end def page_image diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb index f4d0a33d466..2f3d4b80565 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/status_entity.rb @@ -7,7 +7,7 @@ class StatusEntity < Grape::Entity expose :details_path expose :favicon do |status| - ActionController::Base.helpers.image_path(status.favicon) + Gitlab::Favicon.status(status.favicon) end expose :action, if: -> (status, _) { status.has_action? } do diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb index d3debc16fa9..7697f5fe885 100644 --- a/app/uploaders/favicon_uploader.rb +++ b/app/uploaders/favicon_uploader.rb @@ -2,19 +2,19 @@ class FaviconUploader < AttachmentUploader include CarrierWave::MiniMagick STATUS_ICON_NAMES = [ - :status_canceled, - :status_created, - :status_failed, - :status_manual, - :status_not_found, - :status_pending, - :status_running, - :status_skipped, - :status_success, - :status_warning + :favicon_status_canceled, + :favicon_status_created, + :favicon_status_failed, + :favicon_status_manual, + :favicon_status_not_found, + :favicon_status_pending, + :favicon_status_running, + :favicon_status_skipped, + :favicon_status_success, + :favicon_status_warning ].freeze - version :default do + version :favicon_main do process resize_to_fill: [32, 32] process convert: 'ico' @@ -24,7 +24,7 @@ class FaviconUploader < AttachmentUploader end STATUS_ICON_NAMES.each do |status_name| - version status_name, from_version: :default do + version status_name, from_version: :favicon_main do process status_favicon: status_name def full_filename(filename) @@ -41,7 +41,7 @@ class FaviconUploader < AttachmentUploader def status_favicon(status_name) manipulate! do |img| - overlay_path = Rails.root.join("app/assets/images/ci_favicons/overlays/favicon_#{status_name}.png") + overlay_path = Rails.root.join("app/assets/images/ci_favicons/overlays/#{status_name}.png") overlay = MiniMagick::Image.open(overlay_path) img.composite(overlay) do |c| c.compose 'over' diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 8d73bd8fb55..1119c75637c 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -62,7 +62,7 @@ = f.label :favicon, 'Favicon', class: 'control-label' .col-sm-10 - if @appearance.favicon? - = image_tag @appearance.favicon.default.url, class: 'appearance-light-logo-preview' + = image_tag @appearance.favicon.favicon_main.url, class: 'appearance-light-logo-preview' - if @appearance.favicon? = f.label :favicon, 'Generated status icons', class: 'control-label' .col-sm-10 diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb index 0d71ff03163..e6195a60d4f 100644 --- a/lib/gitlab/ci/status/canceled.rb +++ b/lib/gitlab/ci/status/canceled.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - Gitlab::Favicon.status('canceled') + 'favicon_status_canceled' end end end diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb index de86191dfeb..846f00b83dd 100644 --- a/lib/gitlab/ci/status/created.rb +++ b/lib/gitlab/ci/status/created.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - Gitlab::Favicon.status('created') + 'favicon_status_created' end end end diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb index 20e2050108c..27ce85bd3ed 100644 --- a/lib/gitlab/ci/status/failed.rb +++ b/lib/gitlab/ci/status/failed.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - Gitlab::Favicon.status('failed') + 'favicon_status_failed' end end end diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb index 2c02ce6e870..fc387e2fd25 100644 --- a/lib/gitlab/ci/status/manual.rb +++ b/lib/gitlab/ci/status/manual.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - Gitlab::Favicon.status('manual') + 'favicon_status_manual' end end end diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb index 9122d11cfed..6780780db32 100644 --- a/lib/gitlab/ci/status/pending.rb +++ b/lib/gitlab/ci/status/pending.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - Gitlab::Favicon.status('pending') + 'favicon_status_pending' end end end diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb index 9bc48ec2c29..ee13905e46d 100644 --- a/lib/gitlab/ci/status/running.rb +++ b/lib/gitlab/ci/status/running.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - Gitlab::Favicon.status('running') + 'favicon_status_running' end end end diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb index b404118cd3b..0dbdc4de426 100644 --- a/lib/gitlab/ci/status/skipped.rb +++ b/lib/gitlab/ci/status/skipped.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - Gitlab::Favicon.status('skipped') + 'favicon_status_skipped' end end end diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb index be7e5d60b26..731013ec017 100644 --- a/lib/gitlab/ci/status/success.rb +++ b/lib/gitlab/ci/status/success.rb @@ -15,7 +15,7 @@ module Gitlab end def favicon - Gitlab::Favicon.status('success') + 'favicon_status_success' end end end diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb index 8802f58e31c..51a25b408ee 100644 --- a/lib/gitlab/favicon.rb +++ b/lib/gitlab/favicon.rb @@ -1,8 +1,8 @@ module Gitlab class Favicon class << self - def default - return custom_favicon_url(appearance_favicon.default.url) if appearance_favicon.exists? + def main + return custom_favicon_url(appearance_favicon.favicon_main.url) if appearance_favicon.exists? return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY']) return 'favicon-blue.ico' if Rails.env.development? @@ -11,13 +11,16 @@ module Gitlab def status(status_name) if appearance_favicon.exists? - custom_favicon_url(appearance_favicon.public_send("status_#{status_name}").url) # rubocop:disable GitlabSecurity/PublicSend + custom_favicon_url(appearance_favicon.public_send("#{status_name}").url) # rubocop:disable GitlabSecurity/PublicSend else - dir = 'ci_favicons' - dir = File.join(dir, 'dev') if Rails.env.development? - dir = File.join(dir, 'canary') if Gitlab::Utils.to_boolean(ENV['CANARY']) + path = File.join( + 'ci_favicons', + Rails.env.development? ? 'dev' : '', + Gitlab::Utils.to_boolean(ENV['CANARY']) ? 'canary' : '', + "#{status_name}.ico" + ) - File.join(dir, "favicon_status_#{status_name}.ico") + ActionController::Base.helpers.image_path(path) end end diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb index ddfa81c0b5d..51b8fda81d1 100644 --- a/spec/lib/gitlab/favicon_spec.rb +++ b/spec/lib/gitlab/favicon_spec.rb @@ -1,38 +1,38 @@ require 'rails_helper' RSpec.describe Gitlab::Favicon, :request_store do - describe '.default' do + describe '.main' do it 'defaults to favicon.ico' do allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) - expect(described_class.default).to eq 'favicon.ico' + expect(described_class.main).to eq 'favicon.ico' end it 'has blue favicon for development' do allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) - expect(described_class.default).to eq 'favicon-blue.ico' + expect(described_class.main).to eq 'favicon-blue.ico' end it 'has yellow favicon for canary' do stub_env('CANARY', 'true') - expect(described_class.favicon).to eq 'favicon-yellow.ico' + expect(described_class.main).to eq 'favicon-yellow.ico' end it 'uses the custom favicon if a favicon appearance is present' do create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) - expect(described_class.default).to match %r{/uploads/-/system/appearance/favicon/\d+/default_dk.ico\?} + expect(described_class.main).to match %r{/uploads/-/system/appearance/favicon/\d+/favicon_main_dk.ico} end end describe '.status' do - subject { described_class.status('created') } + subject { described_class.status('favicon_status_created') } it 'defaults to the stock icon' do - expect(subject).to eq 'ci_favicons/favicon_status_created.ico' + expect(subject).to eq '/assets/ci_favicons/favicon_status_created.ico' end it 'uses the custom favicon if a favicon appearance is present' do create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) - expect(subject).to match(%r{/uploads/-/system/appearance/favicon/\d+/status_created_dk.ico\?}) + expect(subject).to match(%r{/uploads/-/system/appearance/favicon/\d+/favicon_status_created_dk.ico}) end end end diff --git a/spec/uploaders/favicon_uploader_spec.rb b/spec/uploaders/favicon_uploader_spec.rb index 5989d294112..b521670addb 100644 --- a/spec/uploaders/favicon_uploader_spec.rb +++ b/spec/uploaders/favicon_uploader_spec.rb @@ -19,11 +19,11 @@ RSpec.describe FaviconUploader do end it 'has the correct format' do - expect(uploader.default).to be_format('ico') + expect(uploader.favicon_main).to be_format('ico') end it 'has the correct dimensions' do - expect(uploader.default).to have_dimensions(32, 32) + expect(uploader.favicon_main).to have_dimensions(32, 32) end it 'generates all the status icons' do @@ -31,7 +31,7 @@ RSpec.describe FaviconUploader do expect(FaviconUploader::STATUS_ICON_NAMES.count).to eq 10 FaviconUploader::STATUS_ICON_NAMES.each do |status_name| - expect(File.exist?(uploader.status_not_found.file.file)).to be true + expect(File.exist?(uploader.favicon_status_not_found.file.file)).to be true end end end From 5202c3f0c8da618e2d3821917f6f5d48ae8ae3c2 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 5 Dec 2017 18:47:31 +0100 Subject: [PATCH 16/35] fix resetFavicon so that it actually resets --- app/assets/javascripts/lib/utils/common_utils.js | 3 ++- app/views/layouts/_head.html.haml | 2 +- spec/javascripts/lib/utils/common_utils_spec.js | 11 ++++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 8b5445d012b..310b731c6d8 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -393,8 +393,9 @@ export const setFavicon = (faviconPath) => { export const resetFavicon = () => { const faviconEl = document.getElementById('favicon'); - const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null; + if (faviconEl) { + const originalFavicon = faviconEl.getAttribute('data-default-href'); faviconEl.setAttribute('href', originalFavicon); } }; diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 02bdfe9aa3c..024f80e9935 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -25,7 +25,7 @@ %title= page_title(site_name) %meta{ name: "description", content: page_description } - = favicon_link_tag favicon, id: 'favicon' + = favicon_link_tag favicon, id: 'favicon', :'data-default-href': favicon = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 27f06573432..64d13275a59 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -395,6 +395,7 @@ describe('common_utils', () => { const favicon = document.createElement('link'); favicon.setAttribute('id', 'favicon'); favicon.setAttribute('href', 'default/favicon'); + favicon.setAttribute('data-default-href', 'default/favicon'); document.body.appendChild(favicon); }); @@ -413,7 +414,7 @@ describe('common_utils', () => { beforeEach(() => { const favicon = document.createElement('link'); favicon.setAttribute('id', 'favicon'); - favicon.setAttribute('href', 'default/favicon'); + favicon.setAttribute('data-original-href', 'default/favicon'); document.body.appendChild(favicon); }); @@ -421,7 +422,9 @@ describe('common_utils', () => { document.body.removeChild(document.getElementById('favicon')); }); - it('should reset page favicon to tanuki', () => { + it('should reset page favicon to the default icon', () => { + const favicon = document.getElementById('favicon'); + favicon.setAttribute('href', 'new/favicon'); commonUtils.resetFavicon(); expect(document.getElementById('favicon').getAttribute('href')).toEqual('default/favicon'); }); @@ -434,6 +437,8 @@ describe('common_utils', () => { beforeEach(() => { const favicon = document.createElement('link'); favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('href', 'null'); + favicon.setAttribute('data-original-href', faviconDataUrl); document.body.appendChild(favicon); mock = new MockAdapter(axios); }); @@ -449,7 +454,7 @@ describe('common_utils', () => { commonUtils.setCiStatusFavicon(BUILD_URL) .then(() => { const favicon = document.getElementById('favicon'); - expect(favicon.getAttribute('href')).toEqual('null'); + expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); done(); }) // Error is already caught in catch() block of setCiStatusFavicon, From 9e14f437b6ed205744d916f5566ee2c11e52b734 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 6 Dec 2017 21:04:53 +0100 Subject: [PATCH 17/35] create favicon overlay on the client the initial reason for this change was that graphicsmagick does not support writing to ico files. this fact lead to a chain of changes: 1. use png instead of ico (browser support is good enough) 2. render the overlays on the client using the canvas API. this way we only need to store the original favion and generate the overlay versions dynamically. this change also enables (next step) to simplify the handling of the stock favicons as well, as we don't need to generate all the versions upfront. --- app/assets/javascripts/favicon_admin.js | 19 +++++++ .../javascripts/lib/utils/common_utils.js | 50 +++++++++++++++++-- .../mr_widget_options.vue | 5 +- app/serializers/status_entity.rb | 2 +- app/uploaders/favicon_uploader.rb | 37 +------------- app/views/admin/appearances/_form.html.haml | 7 ++- app/views/layouts/_head.html.haml | 2 +- lib/gitlab/favicon.rb | 31 ++++++++---- spec/features/admin/admin_appearance_spec.rb | 27 ++-------- .../lib/utils/common_utils_spec.js | 36 +++++++++++-- spec/javascripts/lib/utils/mock_data.js | 5 ++ .../vue_mr_widget/mr_widget_options_spec.js | 13 +++-- spec/lib/gitlab/favicon_spec.rb | 30 ++++++++--- spec/uploaders/favicon_uploader_spec.rb | 11 +--- 14 files changed, 168 insertions(+), 107 deletions(-) create mode 100644 app/assets/javascripts/favicon_admin.js create mode 100644 spec/javascripts/lib/utils/mock_data.js diff --git a/app/assets/javascripts/favicon_admin.js b/app/assets/javascripts/favicon_admin.js new file mode 100644 index 00000000000..6b2dcf4502e --- /dev/null +++ b/app/assets/javascripts/favicon_admin.js @@ -0,0 +1,19 @@ +import {createOverlayIcon} from '~/lib/utils/common_utils'; + +export default class FaviconAdmin { + constructor() { + const faviconContainer = $('.js-favicons'); + const faviconUrl = faviconContainer.data('favicon'); + const overlayUrls = faviconContainer.data('status-overlays'); + + overlayUrls.forEach((statusOverlay) => { + createOverlayIcon(faviconUrl, statusOverlay).then((faviconWithOverlayUrl) => { + const image = $(''); + image.addClass('appearance-light-logo-preview'); + image.attr('src', faviconWithOverlayUrl); + + faviconContainer.append(image); + }); + }); + } +} diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 310b731c6d8..d55d0585031 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -384,6 +384,49 @@ export const backOff = (fn, timeout = 60000) => { }); }; +export const createOverlayIcon = (iconPath, overlayPath) => { + const faviconImage = document.createElement('img'); + + return new Promise((resolve) => { + faviconImage.onload = () => { + const size = 32; + + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + + const context = canvas.getContext('2d'); + context.clearRect(0, 0, size, size); + context.drawImage( + faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size, + ); + + const overlayImage = document.createElement('img'); + overlayImage.onload = () => { + context.drawImage( + overlayImage, 0, 0, overlayImage.width, overlayImage.height, 0, 0, size, size, + ); + + const faviconWithOverlayUrl = canvas.toDataURL(); + + resolve(faviconWithOverlayUrl); + }; + overlayImage.src = overlayPath; + }; + faviconImage.src = iconPath; + }); +}; + +export const setFaviconOverlay = (overlayPath) => { + const faviconEl = document.getElementById('favicon'); + + if (!faviconEl) { return null; } + + const iconPath = faviconEl.getAttribute('data-original-href'); + + return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => faviconEl.setAttribute('href', faviconWithOverlayUrl)); +}; + export const setFavicon = (faviconPath) => { const faviconEl = document.getElementById('favicon'); if (faviconEl && faviconPath) { @@ -395,7 +438,7 @@ export const resetFavicon = () => { const faviconEl = document.getElementById('favicon'); if (faviconEl) { - const originalFavicon = faviconEl.getAttribute('data-default-href'); + const originalFavicon = faviconEl.getAttribute('data-original-href'); faviconEl.setAttribute('href', originalFavicon); } }; @@ -404,10 +447,9 @@ export const setCiStatusFavicon = pageUrl => axios.get(pageUrl) .then(({ data }) => { if (data && data.favicon) { - setFavicon(data.favicon); - } else { - resetFavicon(); + return setFaviconOverlay(data.favicon); } + return resetFavicon(); }) .catch(resetFavicon); diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index f69fe03fcb3..d6cba878b28 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -36,7 +36,7 @@ import { notify, SourceBranchRemovalStatus, } from './dependencies'; -import { setFavicon } from '../lib/utils/common_utils'; +import { setFaviconOverlay } from '../lib/utils/common_utils'; export default { el: '#js-vue-mr-widget', @@ -159,8 +159,9 @@ export default { }, setFaviconHelper() { if (this.mr.ciStatusFaviconPath) { - setFavicon(this.mr.ciStatusFaviconPath); + return setFaviconOverlay(this.mr.ciStatusFaviconPath); } + return Promise.resolve(); }, fetchDeployments() { return this.service.fetchDeployments() diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb index 2f3d4b80565..47df7f9dcf9 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/status_entity.rb @@ -7,7 +7,7 @@ class StatusEntity < Grape::Entity expose :details_path expose :favicon do |status| - Gitlab::Favicon.status(status.favicon) + Gitlab::Favicon.status_overlay(status.favicon) end expose :action, if: -> (status, _) { status.has_action? } do diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb index 7697f5fe885..d7be77477b2 100644 --- a/app/uploaders/favicon_uploader.rb +++ b/app/uploaders/favicon_uploader.rb @@ -1,35 +1,12 @@ class FaviconUploader < AttachmentUploader include CarrierWave::MiniMagick - STATUS_ICON_NAMES = [ - :favicon_status_canceled, - :favicon_status_created, - :favicon_status_failed, - :favicon_status_manual, - :favicon_status_not_found, - :favicon_status_pending, - :favicon_status_running, - :favicon_status_skipped, - :favicon_status_success, - :favicon_status_warning - ].freeze - version :favicon_main do process resize_to_fill: [32, 32] - process convert: 'ico' + process convert: 'png' def full_filename(filename) - filename_for_different_format(super(filename), 'ico') - end - end - - STATUS_ICON_NAMES.each do |status_name| - version status_name, from_version: :favicon_main do - process status_favicon: status_name - - def full_filename(filename) - filename_for_different_format(super(filename), 'ico') - end + filename_for_different_format(super(filename), 'png') end end @@ -39,16 +16,6 @@ class FaviconUploader < AttachmentUploader private - def status_favicon(status_name) - manipulate! do |img| - overlay_path = Rails.root.join("app/assets/images/ci_favicons/overlays/#{status_name}.png") - overlay = MiniMagick::Image.open(overlay_path) - img.composite(overlay) do |c| - c.compose 'over' - end - end - end - def filename_for_different_format(filename, format) filename.chomp(File.extname(filename)) + ".#{format}" end diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 1119c75637c..f77e22bcc45 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -62,13 +62,12 @@ = f.label :favicon, 'Favicon', class: 'control-label' .col-sm-10 - if @appearance.favicon? - = image_tag @appearance.favicon.favicon_main.url, class: 'appearance-light-logo-preview' + = image_tag @appearance.favicon.favicon_main.url, class: 'appearance-light-logo-preview js-main-favicon' - if @appearance.favicon? - = f.label :favicon, 'Generated status icons', class: 'control-label' + = f.label :favicon, 'Status icons preview', class: 'control-label' .col-sm-10 - if @appearance.favicon? - - FaviconUploader::STATUS_ICON_NAMES.each do |status_name| - = image_tag @appearance.favicon.public_send(status_name).url, class: 'appearance-light-logo-preview' + .js-favicons{ data: { favicon: @appearance.favicon.favicon_main.url, status_overlays: Gitlab::Favicon.available_status_overlays } } - if @appearance.persisted? %br = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo" diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 024f80e9935..9253a0652da 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -25,7 +25,7 @@ %title= page_title(site_name) %meta{ name: "description", content: page_description } - = favicon_link_tag favicon, id: 'favicon', :'data-default-href': favicon + = favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png' = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb index 51a25b408ee..e28d4c67661 100644 --- a/lib/gitlab/favicon.rb +++ b/lib/gitlab/favicon.rb @@ -9,18 +9,27 @@ module Gitlab 'favicon.ico' end - def status(status_name) - if appearance_favicon.exists? - custom_favicon_url(appearance_favicon.public_send("#{status_name}").url) # rubocop:disable GitlabSecurity/PublicSend - else - path = File.join( - 'ci_favicons', - Rails.env.development? ? 'dev' : '', - Gitlab::Utils.to_boolean(ENV['CANARY']) ? 'canary' : '', - "#{status_name}.ico" - ) + def status_overlay(status_name) + path = File.join( + 'ci_favicons', + 'overlays', + "#{status_name}.png" + ) - ActionController::Base.helpers.image_path(path) + ActionController::Base.helpers.image_path(path) + end + + def available_status_overlays + available_status_names.map do |status_name| + status_overlay(status_name) + end + end + + def available_status_names + @available_status_names ||= begin + Dir.glob(Rails.root.join('app', 'assets', 'images', 'ci_favicons', 'overlays', "*.png")) + .map { |file| File.basename(file, '.png') } + .sort end end diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb index ffffd14752e..0ac4f111c52 100644 --- a/spec/features/admin/admin_appearance_spec.rb +++ b/spec/features/admin/admin_appearance_spec.rb @@ -76,38 +76,19 @@ feature 'Admin Appearance' do expect(page).not_to have_css(header_logo_selector) end - scenario 'Favicon' do + scenario 'Favicon', :js do sign_in(create(:admin)) visit admin_appearances_path attach_file(:appearance_favicon, logo_fixture) click_button 'Save' - expect(page).to have_css('//img[data-src$="/default_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_canceled_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_created_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_failed_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_manual_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_not_found_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_pending_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_running_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_skipped_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_success_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_warning_dk.ico"]') + # 11 = 1 original + 10 overlay variations + expect(page).to have_css('.appearance-light-logo-preview', count: 11) click_link 'Remove favicon' - expect(page).not_to have_css('//img[data-src$="/default_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_canceled_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_created_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_failed_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_manual_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_not_found_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_pending_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_running_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_skipped_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_success_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_warning_dk.ico"]') + expect(page).not_to have_css('.appearance-light-logo-preview') # allowed file types attach_file(:appearance_favicon, Rails.root.join('spec', 'fixtures', 'sanitized.svg')) diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 64d13275a59..2d7cc3443cf 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -2,6 +2,7 @@ import axios from '~/lib/utils/axios_utils'; import * as commonUtils from '~/lib/utils/common_utils'; import MockAdapter from 'axios-mock-adapter'; +import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data'; describe('common_utils', () => { describe('parseUrl', () => { @@ -430,6 +431,35 @@ describe('common_utils', () => { }); }); + describe('createOverlayIcon', () => { + it('should return the favicon with the overlay', (done) => { + commonUtils.createOverlayIcon(faviconDataUrl, overlayDataUrl).then((url) => { + expect(url).toEqual(faviconWithOverlayDataUrl); + done(); + }); + }); + }); + + describe('setFaviconOverlay', () => { + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('data-original-href', faviconDataUrl); + document.body.appendChild(favicon); + }); + + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); + + it('should set page favicon to provided favicon overlay', (done) => { + commonUtils.setFaviconOverlay(overlayDataUrl).then(() => { + expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconWithOverlayDataUrl); + done(); + }); + }); + }); + describe('setCiStatusFavicon', () => { const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; let mock; @@ -463,16 +493,14 @@ describe('common_utils', () => { }); it('should set page favicon to CI status favicon based on provided status', (done) => { - const FAVICON_PATH = '//icon_status_success'; - mock.onGet(BUILD_URL).reply(200, { - favicon: FAVICON_PATH, + favicon: overlayDataUrl, }); commonUtils.setCiStatusFavicon(BUILD_URL) .then(() => { const favicon = document.getElementById('favicon'); - expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH); + expect(favicon.getAttribute('href')).toEqual(faviconWithOverlayDataUrl); done(); }) .catch(done.fail); diff --git a/spec/javascripts/lib/utils/mock_data.js b/spec/javascripts/lib/utils/mock_data.js new file mode 100644 index 00000000000..fd0d62b751f --- /dev/null +++ b/spec/javascripts/lib/utils/mock_data.js @@ -0,0 +1,5 @@ +export const faviconDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACcFBMVEX////iQyniQyniQyniQyniQyniQyniQyniQynhRiriQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniRCniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQynhQiniQiniQiniQinhQinpUSjqUSjqTyjqTyjqTyjlSCniRCniQynjRCjqTyjsZSjrWyj8oib9kSb8pyb9pib8oyb8fyb3ZSb4Zib8fCb8oyb8oyb8oyb8pCb8cSbiQyn7bCb8cib8oyb8oSb8bSbtVSjpTij8nyb8oyb8oyb8lCb2Yyf3ZCf8mCb8oyb8oyb8oyb8iib8bSbiRCn8gyb8oyb8eCbpTinrUSj8oyb8oyb8oyb8pSb8bib4Zif0YCf8byb8oyb8oyb8oyb7oib8oyb8nCbjRSn9bib8ayb8nib8oyb8oyb8oyb8kSbpTyjpTyj8jib8oyb8oyb8oyb8fib0Xyf2ZSb8gCb8oyb6pSb8oyb8dib+cCbgQCnjRSn8cCb8oib8oyb8oyb8oybqUCjnSyn8bCb8oyb8oyb8oyb8myb2YyfyXyf8oyb8oyb8hibhQSn+bib8iSb8oyb8qCb+fSbmSSnqTyj8oib9pCb1YifxXyf7pSb8oCb8pCb+mCb0fCf8pSb7hSXvcSjiQyniQinqTyj9kCb9bib9byb+cCbqUSjiRCnsVCj+cSb8pib8bCb8bSbgQCn7bCb8bibjRSn8oyb8ayb8oib8aib8pCbjRCn8pybhQinhQSn8pSb7ayb7aSb6aib8eib///8IbM+7AAAAr3RSTlMBA3NtX2vT698HGQcRLwWLiXnv++3V+eEd/R8HE2V/Y5HjyefdFw99YWfJ+/3nwQP78/HvX1VTQ/kdA2HzbQXj9fX79/3DGf379/33T/v99/f7ba33+/f1+9/18/v59V339flzF/H9+fX3/fMhBwOh9/v5/fmvBV/z+fP3Awnp9/f38+UFgff7+/37+4c77/f7/flFz/f59dFr7/v98Wnr+/f3I5/197EDBU1ZAwUD8/kLUwAAAAFiS0dEAIgFHUgAAAAHdElNRQfhBQoLHiBV6/1lAAACHUlEQVQ4y41TZXsTQRCe4FAIUigN7m7FXY+iLRQKBG2x4g7BjhZ3Le7uMoEkFJprwyQk0CC/iZnNhUZaHt4vt6/szO7cHcD/wFKjZrJWq3YMq1M3eVc9rFzXR2yQkuA3RGxkjZLGiEk9miA2tURJs1RsnhhokYYtzaU13WZDbBVnW1sjo43J2vI6tZ0lLtFeAh1M0lECneI7dGYtrUtk3RUVIKaEJR25qw27yT0s3W0qEHuPlB4RradivXo7GX36xnbo51SQ+fWHARmCgYMGDxkaxbD3SssYPmIkwKgPLrfA87EETTg/fVaSa/SYsQDjSsd7DcGEsr+BieVKmaRNBsjUtClTfUI900y/5Mt05c8oJQKYSURZ2UqYFa0w283M588JEM2BuRwI5EqT8nmmXzZf4l8XsGNfCIv4QcHFklhiBpaqAsuC4tghj+ySyOdjeJYrP7RCCuR/E5tWAqxaLcmCNSyujdxjHZdbn8UHoA0bN/GoNm8hjQJb/ZzYpo6w3TB27JRduxxqrA7YzbWCezixN8RD2Oc2/Ptlfx7o5uT1A4XMiwzj4HfEikNe7+Ew0ZGjeuW70eEYaeHjxomTiKd++E4XnKGz8d+HDufOB3Ky3RcwdNF1qZiKLyf/B44r2tWf15wV143cwI2qfi8dbtKtX6Hbd+6G74EDqkTm/QcPH/0ufFyNLXjy9NnzF9Xb8BJevYY38C+8fZcg/AF3QTYemVkCwwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNy0wNS0xMFQxMTozMDozMiswMjowMMzup8UAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTctMDUtMTBUMTE6MzA6MzIrMDI6MDC9sx95AAAAAElFTkSuQmCC'; + +export const overlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA85JREFUWAntVllIVGEUPv/9b46O41KplYN7PeRkti8TjQlhCUGh3MmeQugpIsGKAi2soIcIooiohxYKK2daqDAlIpIiWwxtQaJcaHE0d5tMrbn37z9XRqfR0TvVW56Hudf//uec72zfEWBCJjIwkYGJDPzvGSD/KgExN3Oi2Q+2DJgSDYQEMwItVGH1iZGmJw/Si1y+/PwVAMYYib22MYc/8hVQFgKDEfYoId0KYzagAQebsos/ewMZoeB9wdffcTYpQSaCTWHKoqSQaDk7zkIt0+aCUR8BelEHrf3dUNv9AcqbnsHtT5UKB/hTASh0SLYjnjb/CIDRJi0XiFAaJOpCD8zLpdb4NB66b1OfelthX815dtdRRfiti2aAXLvVLiMQ6olGyztGDkSo4JGGXk8/QFdGpYzpHG2GBQTDhtgVhPEaVbbVpvI6GJz22rv4TcAfrYI1x7Rj5MWWAppomKFVVb2302SFzUkZHAbkG+0b1+Gh77yNYjrmqnWTrLBLRxdvBWv8qlFujH/kYjJYyvLkj71t78zAUvzMAMnHhpN4zf9UREJhd8omyssxu1IgazQDwDnHUcNuH6vhPIE1fmuBzHt74Hn7W89jWGtcAjoaIDOFrdcMYJBkgOCoaRF0Lj0oglddDbCj6tRvKjphEpgjkzEQs2YAKsNxMzjn3nKurhzK+Ly7xe28ua8TwgMMcHJZnvvT0BPtEEKM4tDJ+C8GvIIk4ylINIXVZ0EUKJxYuh3mhCeokbudl6TtVc88dfBdLwbyaWB6zQCYQJpBYSrDGQxBQ/ZWRM2B+VNmQnVnHWx7elyNuL2/R336co7KyJR8CL9oLgEuFlREevWUkEl6uGwpVEG4FBm0OEf9N10NMgPlvWYAuNVwsWDKvcUNYsHUWTCZ13ysyFEXe6TO6aC8CUr9IiK+A05TQrc8yjwmxARHeeMAPlfQJw+AQRwu0YhL/GDXi9NwufG+S8dYkuYMqIb4SsWthotlNMOUCOM6r+G9cqXxPmd1dqrBav/o1zJy2l5/NUjJA/VORwYuFnOUaTQcPs9wMqwV++Xv8oADxKAcZ8nLPr8AoGW+xR6HSqYk3GodAz2QNj0V+Gr26dT9ASNH5239Pf0gktVNWZca8ZvfAFBprWS6hSu1pqt++Y0PD+WIwDAhIWQGtzvSHDbcodfFUFB9hg1Gjs5LXqIdFL+acFBl+FddqYwdxsWC3I70OvgfUaA65zhq2O2c8VxYcyIGFTVlXegYtvCXANCQZJMobjVcLMjtSK/IcEgyOOe8Ve5w7ryKDefp2P3+C/5ohv8HZmVLAAAAAElFTkSuQmCC'; + +export const faviconWithOverlayDataUrl = 'data:image/png;base64,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'; diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 30918428da2..6342ea00436 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -5,6 +5,7 @@ import notify from '~/lib/utils/notify'; import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mockData from './mock_data'; +import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from '../lib/utils/mock_data'; const returnPromise = data => new Promise((resolve) => { resolve({ @@ -273,6 +274,7 @@ describe('mrWidgetOptions', () => { beforeEach(() => { const favicon = document.createElement('link'); favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('data-original-href', faviconDataUrl); document.body.appendChild(favicon); faviconElement = document.getElementById('favicon'); @@ -282,10 +284,13 @@ describe('mrWidgetOptions', () => { document.body.removeChild(document.getElementById('favicon')); }); - it('should call setFavicon method', () => { - vm.setFaviconHelper(); - - expect(faviconElement.getAttribute('href')).toEqual(vm.mr.ciStatusFaviconPath); + it('should call setFavicon method', (done) => { + vm.mr.ciStatusFaviconPath = overlayDataUrl; + vm.setFaviconHelper().then(() => { + expect(faviconElement.getAttribute('href')).toEqual(faviconWithOverlayDataUrl); + done(); + }) + .catch(done.fail); }); it('should not call setFavicon when there is no ciStatusFaviconPath', () => { diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb index 51b8fda81d1..22b9c631ed8 100644 --- a/spec/lib/gitlab/favicon_spec.rb +++ b/spec/lib/gitlab/favicon_spec.rb @@ -19,20 +19,34 @@ RSpec.describe Gitlab::Favicon, :request_store do it 'uses the custom favicon if a favicon appearance is present' do create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) - expect(described_class.main).to match %r{/uploads/-/system/appearance/favicon/\d+/favicon_main_dk.ico} + expect(described_class.main).to match %r{/uploads/-/system/appearance/favicon/\d+/favicon_main_dk.png} end end - describe '.status' do - subject { described_class.status('favicon_status_created') } + describe '.status_overlay' do + subject { described_class.status_overlay('favicon_status_created') } - it 'defaults to the stock icon' do - expect(subject).to eq '/assets/ci_favicons/favicon_status_created.ico' + it 'returns the overlay for the status' do + expect(subject).to eq '/assets/ci_favicons/overlays/favicon_status_created.png' end + end - it 'uses the custom favicon if a favicon appearance is present' do - create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) - expect(subject).to match(%r{/uploads/-/system/appearance/favicon/\d+/favicon_status_created_dk.ico}) + describe '.available_status_names' do + subject { described_class.available_status_names } + + it 'returns the available status names' do + expect(subject).to eq %w( + favicon_status_canceled + favicon_status_created + favicon_status_failed + favicon_status_manual + favicon_status_not_found + favicon_status_pending + favicon_status_running + favicon_status_skipped + favicon_status_success + favicon_status_warning + ) end end end diff --git a/spec/uploaders/favicon_uploader_spec.rb b/spec/uploaders/favicon_uploader_spec.rb index b521670addb..db8a3207f4d 100644 --- a/spec/uploaders/favicon_uploader_spec.rb +++ b/spec/uploaders/favicon_uploader_spec.rb @@ -19,20 +19,11 @@ RSpec.describe FaviconUploader do end it 'has the correct format' do - expect(uploader.favicon_main).to be_format('ico') + expect(uploader.favicon_main).to be_format('png') end it 'has the correct dimensions' do expect(uploader.favicon_main).to have_dimensions(32, 32) end - - it 'generates all the status icons' do - # make sure that the following each statement actually loops - expect(FaviconUploader::STATUS_ICON_NAMES.count).to eq 10 - - FaviconUploader::STATUS_ICON_NAMES.each do |status_name| - expect(File.exist?(uploader.favicon_status_not_found.file.file)).to be true - end - end end end From 949c30d42b91a0dd3959a3ca303b8f76158a2556 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 7 Dec 2017 13:15:49 +0100 Subject: [PATCH 18/35] remove all .ico favicon variations, use png always the ci status icons are generated client side, wo we don't need the static files anymore. --- .../dev/favicon_status_canceled.ico | Bin 4286 -> 0 bytes .../dev/favicon_status_created.ico | Bin 4286 -> 0 bytes .../ci_favicons/dev/favicon_status_failed.ico | Bin 4286 -> 0 bytes .../ci_favicons/dev/favicon_status_manual.ico | Bin 4286 -> 0 bytes .../dev/favicon_status_not_found.ico | Bin 4286 -> 0 bytes .../dev/favicon_status_pending.ico | Bin 4286 -> 0 bytes .../dev/favicon_status_running.ico | Bin 4286 -> 0 bytes .../dev/favicon_status_skipped.ico | Bin 4286 -> 0 bytes .../dev/favicon_status_success.ico | Bin 4286 -> 0 bytes .../dev/favicon_status_warning.ico | Bin 4286 -> 0 bytes .../ci_favicons/favicon_status_canceled.ico | Bin 4286 -> 0 bytes .../favicon_status_canceled.png | Bin .../ci_favicons/favicon_status_created.ico | Bin 4286 -> 0 bytes .../{overlays => }/favicon_status_created.png | Bin .../ci_favicons/favicon_status_failed.ico | Bin 4286 -> 0 bytes .../{overlays => }/favicon_status_failed.png | Bin .../ci_favicons/favicon_status_manual.ico | Bin 4286 -> 0 bytes .../{overlays => }/favicon_status_manual.png | Bin .../ci_favicons/favicon_status_not_found.ico | Bin 4286 -> 0 bytes .../favicon_status_not_found.png | Bin .../ci_favicons/favicon_status_pending.ico | Bin 4286 -> 0 bytes .../{overlays => }/favicon_status_pending.png | Bin .../ci_favicons/favicon_status_running.ico | Bin 4286 -> 0 bytes .../{overlays => }/favicon_status_running.png | Bin .../ci_favicons/favicon_status_skipped.ico | Bin 4286 -> 0 bytes .../{overlays => }/favicon_status_skipped.png | Bin .../ci_favicons/favicon_status_success.ico | Bin 4286 -> 0 bytes .../{overlays => }/favicon_status_success.png | Bin .../ci_favicons/favicon_status_warning.ico | Bin 4286 -> 0 bytes .../{overlays => }/favicon_status_warning.png | Bin app/assets/images/favicon-blue.png | Bin 0 -> 1522 bytes app/assets/images/favicon-yellow.ico | Bin 5430 -> 0 bytes app/assets/images/favicon-yellow.png | Bin 0 -> 1667 bytes app/assets/images/favicon.ico | Bin 5430 -> 0 bytes app/assets/images/favicon.png | Bin 0 -> 1611 bytes app/assets/javascripts/favicon_admin.js | 2 +- .../components/file_icon/file_icon_map.js | 2 +- app/models/project_services/jira_service.rb | 2 +- doc/user/reserved_names.md | 2 +- lib/gitlab/favicon.rb | 9 ++++----- lib/gitlab/path_regex.rb | 2 +- public/favicon.ico | Bin 5430 -> 0 bytes public/favicon.png | Bin 0 -> 1611 bytes .../projects/jobs_controller_spec.rb | 2 +- .../merge_requests_controller_spec.rb | 2 +- .../projects/pipelines_controller_spec.rb | 2 +- .../user_creates_image_diff_notes_spec.rb | 2 +- spec/javascripts/jobs/mock_data.js | 4 ++-- spec/javascripts/pipelines/graph/mock_data.js | 18 +++++++++--------- spec/lib/gitlab/favicon_spec.rb | 10 +++++----- .../project_services/jira_service_spec.rb | 2 +- spec/serializers/build_serializer_spec.rb | 4 ++-- spec/serializers/pipeline_serializer_spec.rb | 2 +- spec/serializers/status_entity_spec.rb | 6 +++--- spec/services/system_note_service_spec.rb | 6 +++--- 55 files changed, 39 insertions(+), 40 deletions(-) delete mode 100644 app/assets/images/ci_favicons/dev/favicon_status_canceled.ico delete mode 100644 app/assets/images/ci_favicons/dev/favicon_status_created.ico delete mode 100644 app/assets/images/ci_favicons/dev/favicon_status_failed.ico delete mode 100644 app/assets/images/ci_favicons/dev/favicon_status_manual.ico delete mode 100644 app/assets/images/ci_favicons/dev/favicon_status_not_found.ico delete mode 100644 app/assets/images/ci_favicons/dev/favicon_status_pending.ico delete mode 100644 app/assets/images/ci_favicons/dev/favicon_status_running.ico delete mode 100644 app/assets/images/ci_favicons/dev/favicon_status_skipped.ico delete mode 100644 app/assets/images/ci_favicons/dev/favicon_status_success.ico delete mode 100644 app/assets/images/ci_favicons/dev/favicon_status_warning.ico delete mode 100644 app/assets/images/ci_favicons/favicon_status_canceled.ico rename app/assets/images/ci_favicons/{overlays => }/favicon_status_canceled.png (100%) delete mode 100644 app/assets/images/ci_favicons/favicon_status_created.ico rename app/assets/images/ci_favicons/{overlays => }/favicon_status_created.png (100%) delete mode 100644 app/assets/images/ci_favicons/favicon_status_failed.ico rename app/assets/images/ci_favicons/{overlays => }/favicon_status_failed.png (100%) delete mode 100644 app/assets/images/ci_favicons/favicon_status_manual.ico rename app/assets/images/ci_favicons/{overlays => }/favicon_status_manual.png (100%) delete mode 100644 app/assets/images/ci_favicons/favicon_status_not_found.ico rename app/assets/images/ci_favicons/{overlays => }/favicon_status_not_found.png (100%) delete mode 100644 app/assets/images/ci_favicons/favicon_status_pending.ico rename app/assets/images/ci_favicons/{overlays => }/favicon_status_pending.png (100%) delete mode 100644 app/assets/images/ci_favicons/favicon_status_running.ico rename app/assets/images/ci_favicons/{overlays => }/favicon_status_running.png (100%) delete mode 100644 app/assets/images/ci_favicons/favicon_status_skipped.ico rename app/assets/images/ci_favicons/{overlays => }/favicon_status_skipped.png (100%) delete mode 100644 app/assets/images/ci_favicons/favicon_status_success.ico rename app/assets/images/ci_favicons/{overlays => }/favicon_status_success.png (100%) delete mode 100644 app/assets/images/ci_favicons/favicon_status_warning.ico rename app/assets/images/ci_favicons/{overlays => }/favicon_status_warning.png (100%) create mode 100644 app/assets/images/favicon-blue.png delete mode 100644 app/assets/images/favicon-yellow.ico create mode 100644 app/assets/images/favicon-yellow.png delete mode 100644 app/assets/images/favicon.ico create mode 100644 app/assets/images/favicon.png delete mode 100644 public/favicon.ico create mode 100644 public/favicon.png diff --git a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico deleted file mode 100644 index 4af3582b60d2fa40201791f2865491fb84e0ae76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb_fYfM~K5WY<{8l$Pk#z$hc{<+VEZi8qDO}ZFM(%MGK?qX?wD5W&Men6EU2(eUa zV%w~VA;i)))Ln|DBsF3}h)EOsTO0U6tnsmSVS#Lmr9dd$ecf@seRa89c7gV6hI7xH znfd0--e{Mvw*)s&y#}ij35Xy6SD*d6Znb2dk=cJ-R|{@qU;hyu|t;SVg8FKcZ>ciw_zX_84zBV_atpve|6$nwlDFZf>UI$B$EeeLaveT?Mn0_celQqJ ze?CBt)pne=mlH*srTeW%w}e0%%&3S)nG#bcNA(l7ry_zy?DIa_SeW?AWn%EhlEwnpM8Dm2g+% zz2JK4$j0}3x4tpft^GN9Nu$v&g@WIywBV5`{*_3vlOk0XNmRaL2gT#@j5+ip>JqdP zY$)QrfIP>47x1~p9=_o#_-wpe)kEK_H0o1m#HZ5iH%bOO`ng1XJEVkOZ*Ok~KO7Dd zd_WxXof}GcFYsn@?On$^_D^T=reK#gFxkWL<})X-dA{AQCb3WO*-wcS`B*Ko`~N#kbE0dIi^cg&r(RK2CKF(dYemg1*<`RVm! zU%(G5dc`hN^-g&hZ+A9k_V*uMcE{9N&4_cK1202-cH>K#sG7+o5xFf0{CzTonk5?e zP$b{GBAq@U(&ajlemx@6WQRt7abIWSnZGAm_dKQ_QvNn-Wr`l;T&E~rCzD>Q{H#}~ zJ9SqM!hWsV5Ban%W}{Yu{*7C$j>pZI=Cxj-wBvl}u{7b#^A)x#v#iy6{w>>Ha;5T= zvH^SgUgeCHyqTxFY~=>I~Dd7|JS2Cm2*_ZF@!$P3r_8w zTzKZa1-G{Uo_@lAuURJ7`mR&kgB+w{KYhD$`JS<|Gg(>mJfHbC^pX2{m#xxDZaLoc z{(`5XmC`u9EmPR3^e-oui_bax5&BV=9Gc*o$R%sF$GBZot&r~24qNT#<1M(<8tAi~ zo_w-a`}0nuz_^p5vKJtEv%zJf&{uM+#~kIV2UwKGXdt%C3p({|9+ Uqp2SSjs__C8=+~M$^63fKe{^4Q2+n{ diff --git a/app/assets/images/ci_favicons/dev/favicon_status_created.ico b/app/assets/images/ci_favicons/dev/favicon_status_created.ico deleted file mode 100644 index 13639da2e8a343576006037c7ca2345beee1fd20..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb_fZERCj7`_CJ#%Rz;{76Lc&)R$2!3>%F7zz7fOhAZZ?Iy*jVM7xmKNRE##>k>E z5y*&!5E(2$*G+~AhQtsfU?dUt;fA_^9sB6$+I8txx0Qyqx1X=?b9Xzgx3}#koaXK6 zIp=-f=lwh<6@(@HS-Dccx6uB$AUq`qf|-f8gi0ptD}&GO_aLIBSFT*yc=hVlCZ_95 z)4Y~>9%Z`3w5zwb_xbyc3w<3O9h+FcoAIc#von3;#tj-88lsVr5xRNvCSALBErm1Y zN%DNSySw|z`(X!sW}S`D-P6;Pnw*@ZbUMx3xg#2lDmF2HhRM%1tiD$pSWgSEhlhtL zlgZ?Q8tg7Y=<3-N4fhzu#XF8#1s@-zT^q7=N@Fes9y7WxiJH5%-U~6&!s&`}_Ng;==~i zq;}wr;(fUgzUO3Fd7#bO6Z+mlBWFHPaN_Z}RtEg}zt7gg(HV#lE|FxpeqM`3k?w@>8gt;@Xv+oB>D6!j2sVyb+*V$I;WWS7bD@GLg!kdL!;n-|~?;dCAuE<%df}6z-9+N1! z-$W^3gE;nyx;gyWx(e$0Eo76n)@8No+tJ_ z^QD={!DKfP)ff==x0*>lxRxeQI4IzHpL}f}Qt;Gv3Lo7>)6J`Bko#JTr~f_Pu=x>r zhd7j}H&e2XbFEVPUDjJK{w!BX8)SO{!g;-P9{gEZ%$a%<kvYS8F_DFQKH(^v@l~+Nbrr zM(c=U&2r|SfZV(e);C*Yea(npzZa^jcr0Nvdr6fi?zgyI{6WuGXq~W$XCMcB=wW-t zW~#~-{I5s0iOp2YF@!wNF`MO`LU{VKSdC@dU3tZRr=F+Kde&yyj2x)34^LIE%G)zW zdP3D}o@Jk>AdlQf?bd1|zI=P(ZKY2nt4#rV!%T6Tcs?Irp?%KTImjpN=HNKjL?K?I zGsbn1LnPT|dC%y)*j~&oZHGMTX)nfWbRM-ywQ0L49eE%_}l(JN6=oecVdBkOC+qPjIfMk;UT7_6c(0{S0RBe3wMca$uOmu p!c1PK1|}Y`6Ym*lS%V;ZheMTuvx92}``WJpq)k6RP diff --git a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico deleted file mode 100644 index 5f0e711b104b2e4bf88a3c726cdb9b7f17a09844..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb_fdu&r>6u$(G#%Rz;d?cdK;o9DIJc5hSwWA0zK?iK>-C&p~Zh)GgW)oC28AGCB zkP!$$8L!!m$?ydU8HR*J{x$eYPzC|u&xZ|-RV(LK~r_b?w4j+T+MkF*M&T* zYe;8x$vk)KEUvRUi?dE=aX)#daiNd(mgHHT7iSeYNx#S)d$qQYKHT0}ASZ98E^Ics zi*3I8Hs`f^^z=o@V)xLNU4aSK^Z;52#Jt50k}zqx#?%(C6}``yO@x7krRmkxQ9@4L@iN+z;%} z?p*jkH!qmhbJV=Av94axIXn{6Qm3rdZuS4q2KXV(!hR^I^SKR;Qw-AOs$HX zU1258z=Y3J{gBB9+|}q8YWzQ23LoofGau_eW+HF9k$i_l3h&U9Z`*SU{+=U4Y4}G^ z3@g0Ucqj7#brDjf4DM?53)FKezS}W>iMQQ!DRA6GJ#9wn{Zgc{CIiK`=Tme?J}qB- znqFAhMKO&OfDQ0N>Ofue`qD3G^b7b3eG~YcV|RD87S#8(i?aVKk$PH<)ZHr5$R6Va zcH|R1ohVta$aS0=PKCLt_b{7-o`t@NKIS@7IHR}C^h2P7{mo}quz4QbCyrw`^4WLh zQ}{gtMa%MOh3zyIEXiDlxr{;2QqIo38)tVr3hG9_Gb^}BZ2tj~B8_^A0vp7>4_A-l zdrFIGe(`T)DR*bQ3o_Y&o|V9E0yc)bb*{zRCdPW%=ZLxYh)AKGdWxlBN4DuHQpxt% z*%qer%aLpRz9TKfPR6V1#I+g_m0G3AU=Ei zQv*>IlcXoA&>*a@GLXOidFrfPLoILC(dX+vrf)X9ML&H|MuFxzbeZcq6;J){Ygl%l zyw>Q9*BB^L&9P2UY@LDR8sizcR9q@cSqS_Zu@n1~vY6vFddPPTG}!J}W2(R9QsX|H z4>gt~oO!g=T&dox^*y@6Tt-qNP6-{*CzlxyXzf?8$&&fSIPeoW9+&bdV%Pt3t?JoF z)qAzRN2(S~=l%VVo6y1fu2+~}*4nRL3se_95VjjUl#nN`w~%D~UcE2dci3(`4msdM z58LCtS~_pizj|c5@eozA44_jS8z8Dfy|eQ>aR zPR5$i(i13O@Hn4&5b}uqsAMkJ+Lvi9xVqq>NV&d`RvIW|H+E*)mu;P6b`tUt$>8^K zPGsAw#bewysxp#nH?7y==UN++#MdFudOC9L)#8uZ#mbnZ4+Sgc%+liLS{q$eI3p_Q zWwzs$Tzj?nBUQHP{>q|{2tV6zarmX8&HaThrJyi{JPHYPPPjpA tOPndn6lC%+H8Amjo#%*_*Yr)5D4)?qF zo^yWZd2e8tMfh93oT0yr^JRv4jbRuL3AY#{6799&)LG^(ChaRObj7H-#h<&f2p&>=zNE3{Ub+?fY<9yh{-eE4|GS(hkvPEzhvaJ_V9sEWByD{ zO(peEjm6_A&lfLVOrFt4adC0-oN7`z=#HXV&W7(gr8A5{`Fv1P zQj(0dGS7iP01O6062GOTMF}t8lj#3sCj9^GIrZbGg(IHMTbD0iPSHaP ziaW(2FE0-q4hIYj3_xXNWfD6*E+j*DHP*QYrv#up=iSfh(sbvb zV~ss~t0Sj*%ppkLp9L7{;9bbkSJ4GKY5_MTxS}N=A?d_G{ zhV1k7v9MC7 zOX)+UF%uIL^4**1U7|OOe8=3Eknb*g&d%wxS{XNi?L8(yXdeq1>Ghz*q;GmJ=*=Q*jC%*gJ%-LO1E-A7yY1qQamu@0fcxLa>n(v#vELQ* zJ%LX%#es4`I;ke9FQ_-^exO>Qdy?*IdM^ZlkJ4R(G5<1wtDN2T92?y+_uAt1mcXW& zVqY^CKVeI6VXQC64mHZtaZpjrU8A>KiK+blvr}iBJT68f*yn_oB=kM{EeBA6WMKiU zDn$F6Iq){I&|jy4j!F$2-Kv4JwHo++rv`lOBK(bgt;9?J9&asuL8{^ZiB@wE+J?D~ zQ>2yysha;yDiew&OBSN@YN3z#6S6c%t67r2b*I(%VmfB}S}NlYlYi7>2_m0o%CzO_ zylU&2kF=Y>63G!Qbg6Zb3Le?6U7Z9QF~UyA&< zNiL}a^*yj^KUCvS-}7zDe<^6@+z^)s?6+A9|4TYwwsp|Vcaa?7lOD9kbFWOd;Qx9g zGk*fgF@_|MbHFTqk_|8YEMOJ4Jd~I1x2N-DTR$+1rIdqs?EQC5nR`Y}kI$rk6Meo* z@|61-i`JyZmub(xCFhlpi5-KFIhZx`eVO>O?PJbiiCsg hr1y+yNh`y=gS3uluqFNjcZ(ZhzhUshRPr|q{{t;Pg#`cr diff --git a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico deleted file mode 100644 index ed19b69e1c5bef5fc97b8d8273886f6fbbd028f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb_fZERCz6ux8{jnSZy_>qX>pSAb40}Pq`7zz7fOhAZZ?Iy*jVM7xmKNRE##>k>E z5y*&!5E(2$*G+~AhQtuVhml0sSBEnhZp^Kt>po~#w{~S~Z$FRcxx2U2_O=_sdvp5U zd){--bKdjuzNsKA;b+AP0lz})lY;P!AP7b#W(nm?;46dI9`qohqN`W0ZoGEwS_4xr z(=>m}JdZG4X4=)=-TmT&#)ZAMwzf@dzmM^#y}dnoY{i&p0h5?vf_gphkMRNM@PpC zj4k)|_2q7w(GrOSaZM2Xo12@aoy3O(I52pDZ>89Q$?9@%#OGO``|2_!o*144^l8 zKbIGa|Hs8c-vb}M=NqUwbze#z^2vdZ^MUb47Q*jtShLjAY(CQW^KQj9YAoN##77?# z@)c(k`zja0cb!~aI^JS-`hPUh@R^Sk{Ae_q4_@K-_xGn`fIf`HV!HBb4Nm97AGVk5 zoM@J2ysZ*V9%X8@DR$963k}quRtNdc37pj&|K4o;zfP7u=W&=%`%ar^tVN`WMu}nv z3>5zMRR!PU@#wpcaXugh$PdnH)L&bha^J}u^G`M0%-L-(smB=;IIPy2hONbZ zM=rkRi*N5cN?*VaJ!=2`5_MlX-iy0C6EpL@-?6%U`nV~TZu4XE4skG5YovG$ z=US!Y`)s#XydYOd8)Rz%!g{TA9`c$l=2Wc#`nM+Q${)|h%>I@u#3R@rdQ2niIb30` z&X(2t4!>#MLRK|Tstwqaw}{8|7>2V;`KVT$#6L^z8U6*F4XA zo`ODdAGVq+_2lyLg4;@-idP!O=`ABgE#mona)tPuvvbgoTaDfcu8BgjdTWg9Ae%_C z#q@#R`a-;jRoV`Hw$r+htloOqB2_1?hG?*A<;!~O3-Q8ROP43C2ATbMdm&l9^^mQ6 znYX&^Gs55Y|2e$Yvc0}V_AQaHo>Ib6l7&Z@iYO>7A-6&TT@mgQ`;uZxFa??1Om$2= i;3w`g(9$|Vc!_B}=*m9zA@@Ev%|9a)q?z;+rGEjuZ13y< diff --git a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico deleted file mode 100644 index 5dfefd4cc5a284aa507915ee8dd8efb3ab2a34b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb_fdu$YC5MP4jsDa=EMU5yXq<6b_Eof6QX${pF6Hv76Z3_n`#uj{_e+b$?G(;MR zK?(hhU_Nb-Z-hR8|%-(K$x!YbJ^t()V z_nZ0VH?MCd<+yqHTd{(pzg)vz9Cts*aW*8};?j|5Ut7qV>y>>1=37eG`nVESVR6f? z3*BsBftyJSOIb&pn>ATVm7Vbw@W@=_l0Hirdl4}&#g)jgsEjG=zE-;ob(}!^drn6n&E7~6U zYPPhyiTz=^_+sU$GQz(+5UuTOM+l$zvJSWLOb9=($sC{h1NEq>@PDgFUea0TaPK-= zr1>)_%?&>FD5~XH_-)mzQ+nzhzxJK5Ls7{d4ZqtYpR@6w`Xu%0$?*U7B)-#IC*1Tm z2ymlZfRhKeYxw8GYRrhlZ1Aa9)4Yhp|D!tPfzDdT@$1L!&|S|%?@1)7vqt6;PKKtE5 z2zx*JejovY@7aK5CcsnsF2X(KLDPE}8#%YXzkXV@^5mt`cDEfEAQ5 zYifZ<}j z-VA(-eSXMy1D~E02g(KMq?+7wK>@m()E}rvQJwH^Rl$2s zxgICr-e{coDVGrUC>vV1b^YZ4#$gp z?=9GHr?i#7s^-~1&c$5oq-?W+l*gZyGK6)K7=wtPC!8jE<19VZJS&}Vzfq9BC=xUB zTgu?w=Dd^D5!%HLi*YAUm!9 z4!hPYiEYAn1+-e|Uc^Tc`0eov%KtnXV+`v%H^ z9{WIlR??(*#!OFNR`P@Bb3dJ@+>eNkEHk;u_5zy|@0PQyJ+RRRV^03`WOA|gF=waf zye!)My;u{mWX*VpTMI58B&Yp#GyYV2qoVK{oku+lQ^}g~N1Q^AB3j1+*-4L?@u%7w z*_5)JiB<{i*fN!@8Gp!?zSN(S`XSK2?Xz5dG4+G%)4aC;+*(k%B_MGYr1=ow=7CQm z4$g5Cgtn+i3@L!*Ln=VRMRw9WBU)0xagQRcB|5iT|AB9}4?@2V2*Azp-x&N4kZ+6h diff --git a/app/assets/images/ci_favicons/dev/favicon_status_running.ico b/app/assets/images/ci_favicons/dev/favicon_status_running.ico deleted file mode 100644 index a41539c0e3e8fe178f3ea88fbce6c66b00df3c2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 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 diff --git a/app/assets/images/ci_favicons/dev/favicon_status_success.ico b/app/assets/images/ci_favicons/dev/favicon_status_success.ico deleted file mode 100644 index 70f0ca61eca731f254935c0babc2d4002b0c7518..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb_fe@qj16hFh3Ez1_S%F`9EX(GP3~BEGNpuLhWSZ#wF{eu= z*SSyHFj{dl}_pU!I?a|gBcX{01 z`@Zk{ykFn`*$-<2V%HLO&2L-5CwY}%V|ZPzKVa2~EFZ>e!l9mTasW}qIZ zCX+uG1JgJ;Si=hs99R!ok(SOJTm!3)Lj$P&lsoAlVr3mehH) z=jXnYF)9gr>*+M+Bn7)s6JMMIk!;i!+Je~?=CuYfZB(j0} z1NEpx_#M^RlY5UCs?0}c$akwSDp9=Gnv4Q#|_3DZ6ubMO0I*zcn`x{uev7d$WmFcoi zU9R85IPqR0ZCKYfKSq>#HO-4?{L6=PUh1tg92+>sfxTW2j)N?Cc4@)6cczTr)cIwM zu8A0A!^+LkO8hgB_HuIC~^{m)`l!e|}JzT40VYosc#`b=yg`YMC zYM{NR@qyuKUeL@6%uPP!-$OZ?zB;bW*?{pzpJi-(x~kX!_D=MDcLsPi>cF=&13ucX zffdaeG3PK@gJ@=v?=-u~_il6MhT$fIj2pu4Kfr=_rxtvKO@y)ctPJ2gmd}KedJU}o zcFujx2a&AAw!q{#t_#?2rpJ~A>pgl&k1*voO4A};X zLwip{!g`>2K{G3ejkB9i37|7f`{CRtZDy|1L3uZ`FtRsTZy|h&{pNu0A$)pL94Hs0 zlWLOA1)WXm57eWmPg1X@d4au&{`U|C>(osl+{mZRoVka3%wT1(-a^=+*q6oPhio~! zJ{`6kOby1BVmqEzSTyi)!?a5@yCX3p->=kV<=(2{BtM>W%5|8~cWH+Xpa{vN1>hBk z_=P%fp{?hu(xG)<8Z=j@!H-|1!R3lq;ARa6|KPcf#EX7+lrMQoEYo*Mr8@Ag#9Rl- zze)#Uss1}Lk9}J-#UWZRWm`!;w3nVzsh0Nl+$hg|IvO+jTg=l}k$-f?LPS2h^9(|? ztkSpp9m7&E1@jctLHfj{`U6Vx(LB*)cvB+$V2w+r4DcGYH%E%*ycjL3^gUdZJq6_l zXkSbuJu7AeU`^PDT^KI1X!Y!KWJc;3cL`sQf4c;6nQzLEA3KIuVw?1On(N&o5* zjrxNiU<_$LK1Yq*ig;`epZ^&dLo{?Fh}DvX+^Z7IFYQ<`fyS1 zlzw6EHlUyF4|w&P=58HG^4tP23qWEfgUCFAlmbJ{B(TZEz!~Nqp)C@U4`~R=hE$G( j7uiYkjA(K>!%Rb3Ky+q%@CMs<8wBoaFa)>5_Za*Ssj5h) diff --git a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico deleted file mode 100644 index db289e03eb15e0b5d799c1e58864e5941ca66da0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb_fYitx%6rNHH#z;WLR{|Cj?e5HOAEq|`C`x10M3mTew+n?INDCweV?q@Z{2^5$ zfj*#HXrzFJ-EE;Xk>C#`CKW@k&C&Q7<`GdX*A z&OP^h=W*{nyMmBF&+63ze+muD1>p%n5Uj)`60(U|t`f~HI5i?saATct7&Qr%Xj$aO zFpV;e9YQ6pki3}YpILBRHdu+b39FSbu%rs%*Utk*-vb{s0j`U{bEgsFInoJ|?h)ZA z9$OGUA~9F6?WC^|^0fiuAsABIFtGJI)dz)g(j`AEzXD8#4Pj>qf9eGu zuR(}%F?L@*RAM({?`fU|p$h8%0QUY2@Te#MXv7CK5BE}wDy-zaweA8GMQ>btPjmz9 zACE@Fc_15#x*z6S2-@RhH+(Z-WH}-u+bTYfu}Iqw!FipFt603K5+fVFjb$|=KX47$ zav9(;7V)0s98nH-#hn5-*6^CMH)2_xpIkkgd_TlVxyQcwlJQ%I%y9}X0cmvrk7mU8 zgL+5Jf&Xhw#jd-a~l>1hJ`q*E!nX9^dwo8Kzly8bz z#intnT8dyFq}a=nG4-Jp!Q5oLxbHfa9u3DlhoUzU8;?fBJxleGdN;@3?nx;g{o1bL zMzQ;jO9&pcAjH_rl>4bdD?)^SbMvcsv^;>MlcAVC7~yisxMyj3uV8GZc5m96Zl4tH zqMUQhyK5y(mRS&JNTCh#tliYLpZ1VlUcNE`~d{=Y7SX`wj z>Rsc;M^W6Fqu-NK!FSBa5pBOkv7@=)HXEOP2ol%r*rlEeKAT#6{~MRPZ{wBf<`(+R zv2rHn%_#Xe{3ecll@7UtWgV z@{jRDSpf!4q~cFH*E8|--+N2eKO}DxZz@H!#|x>}8i%)AAs304t(Xj{)WQ%R8wZf$@R7TGgk1@lX_jF_tAok6{O$K>!xjxy*v5#w~X}lvcbZvrBe>8 zAKH4*d7E^K=kTdai+%`@m3MYRofZWM?oh<7tX{h}U!N$DQ^(BfVI%z^0T(f_au6 zY_MX|AzqB7XC|kbUEuXWr?sz__JowUY6PFMZv7g+}n3t6ZLK5*>=BB;c1OHw> NB2NbbxIO($;y(%}fBgUe diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico deleted file mode 100644 index 23adcffff5082fb84b1dc17f547875d7d3792251..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb`IUrbwd6vr>Y#K)N!eKJb2aqsW8jG=KRn3#eS*jU}%SW(AfhDvc|2gCFM6QU1G#1|7@K(lNQCM49ZT^$Im-CSGl?eTms+~)T7mbM$e=JfZw z=l473bAIRdPm;>`S5YD1U;6F|NqSn6B*DTesg4DDqj2AD7oX2puBvLYEX(6Uh#TBq zR}>||b%5*ZtpC{Uc0aq@T=-+2ml?O1K~+^%dV702`Tc$x92}&st}be9Y}C-l<2Jb- z<@(9phy!zGUqkR+Uth1y&dyRIksznCyu3`ky}iV7rr9RQxj68sF|e;Gu>1P@D3wY% zVRLj#OG{K+TdQ#l_Z}q%X7_+SG&GdMD5MC5LS_uXhgpm2>gs2hpXli5NEc$6{lQ?+ zY#KH4fVIGWD0Lm!W}Nr69tZ@od^&xk(`oAI=^^B5Vqzj|2=)Wdf3uWxQCC;@7_&FJ zySuY|Iel2!;6to=z@FUJcNd$3y)V4{Ds1NF=A4ijof8{*uos+7UWe<&@b`aC9=Y?{ z1Mm@hzLJj(y}jUmz#UbHFNXDz%1J#fw~rY)i^XDgSH^cKY#88gP;Q;p}&wt&C@A`s#$~RilU{5i-m!HXPut z<}+}^&R2%DYw{H58}o)-U^&Oq5jOavqoY~;fq?--zrCN0Gv~D4RwLZicrVyy+~n{5 zlJ+TM+MNs+>`U8h@L81VW7N>lNV=|P+~GWy!iFK<3)pb}o52?s>0IT6re(3OFtf|T z|1d?OcG9q$nVHG(lgT6@2IQg8ykUg*0&f=Yy<2$4luO!=^0$_JW8PO;KC`ef|99e~ z9{P*K{@1Cd=J_4ZVJ0|-2Hq^po!8!rxyx5|J!{_J_uJUWy*IqY_biZb9VAa(eaSK4 z&Dv%UfsK;0<`;9>GUpz1H$}Yivk?RGfVHsh2Zt#W-V3~0U~}H5koPS*ul=w#o|*q; zW_RZDmG*lXb|wbof!6{)8yXr3=K^OF_XF-I+>^Mgu{TtgixlC;+yAM_HES_U74kH% zKkk@Ma@oedKNiX1XX4;JsT??N#DQ4sB5Dqj{H8x$>AO&4GVQfIrr(wQ&TRRk&AeR)3T-L&NruAEVaQ|!3vPzC}CjN)ox^bnDh_-)k zI@+^ex#Rwbm1l3e+u| z%MSbfNGR|7WVJQh>fzfdyAR=wteX8h)uLtX`yAVo|NOd>jlEAjvASUQp|-3|+s8S5 zSG^T|(Y^+*x1+62WAo`;tsC!V^Mh%uZNBk#y?XRcJY#=2oU->9`sTZ*Ol=Bpnm%0K zzc&imSo@Wh)j87^xwQ5b+ZJ!!{oV9|H7(W-iy>Z!FEaboS4V#hm;WBcFKQ|psX zeRds@F1gaH5Ob5QI>g@eikms6W%k(9+v0&CCOvR Y`FYPgt(-R{Kr0lWs39?mQJj+WKX(M>%>V!Z diff --git a/app/assets/images/ci_favicons/overlays/favicon_status_canceled.png b/app/assets/images/ci_favicons/favicon_status_canceled.png similarity index 100% rename from app/assets/images/ci_favicons/overlays/favicon_status_canceled.png rename to app/assets/images/ci_favicons/favicon_status_canceled.png diff --git a/app/assets/images/ci_favicons/favicon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico deleted file mode 100644 index f9d93b390d8fe6dd63003ea7ebd95c057e7ebc8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 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 diff --git a/app/assets/images/ci_favicons/overlays/favicon_status_failed.png b/app/assets/images/ci_favicons/favicon_status_failed.png similarity index 100% rename from app/assets/images/ci_favicons/overlays/favicon_status_failed.png rename to app/assets/images/ci_favicons/favicon_status_failed.png diff --git a/app/assets/images/ci_favicons/favicon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico deleted file mode 100644 index dbbf1abf30c354d547fe599db7e884815f7ef7e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb_dT}WJ45WXRWKBiFGr_u^Z?$53(U&;xRgU9CB;4Y%kZ4~Sc=o)9-EQA2Nm3Ed^8=zN-p2K+ zAP7O6ojBjX{ZFk{>+8=Om-Hc?4KeN_1|%jXhAS&8p}DykE?l?(_4W0TpPw(&8S;g2 z?!o!x=V1rk8Fh7#?(FPrd17J$g25n|jr;fSLsL@|pq&wvSwdeNepVY$*AQX1w6wtH z=B62J)ZVRIw;(+|T}B%oKZ^~B{RLrnbaX^v?6l!>xl|jL{!c8Dl9FCS{9sj8RU{Tx zS63G_Ha5cY^0Fxl`GI0V`LNqKpv(Z~b=c{2>U7_`cMp1ddtrWl9*!J20_2~bo*uY! z=MIdGjp=ylgYp6IAKJ~k$jZuk0kPK_8X9!^R##UcEiFyKBwRBZ9UaxlP)=_1HN@+K z{cpXKLo!oSQ#xJrQC(fF>Vu4o3@9lnq5Elji;Ig3N z2j#xQ;eec+90jYUrUpWxka9dbJ4riXHj_4nM|oOVWw!oouBoN|&KRHG=DW8oj@hF651@E`oJ4E$B3k|9MGdk>Gt z12&sY^EDgi_4Rc~O->oYRr+_Mtq@DzRnM!zv~*Rj(-e=v}eH z=krlb(XiNfsB5av>>%GzzrgbW6@UNNa6ssnA8NRSeR&(3@R5Y16Ofme58-f_RR^uD zt#I_{QK+r0)nb8f8*+1V!D6w%^z^ijpX?#q(Emk*FJ6N42d~Pq4*LpX)-(7un;@nS zDstoF<2rrxq2^_Aak0WPIXS8H2IJeKH$y!ObMG$oG2ya2$$!uIo9=xL=}QKi?*Cx` zq})G2JX8$H$#3f3A#04+>s4xRtiD7&i|&rGx6$4CYvHi&-h|(7z$V|@R<`gnZ-Qt^ zh0Lt%m^QHXuxsjB+t?k1O(QDim!h$G^gZ2OB{Ab?#Rl>N#ln~mEGDg~Ur^5?Z1npO z`F#r=W8WzcX!q|!?7C=tq4KeYt=T|+z*vyZyu5s+-_YBn`arorb&_f|^$Q7eP!O%u z`#)E4)miL>1b!IfPc`PVXly0#Pq?G-H9IgTg~KNmJIEFziOE=l&NN33+Ak)nn8um! z4?p6Y&9V8~P)uyq2AcUQ*H(=>iFMNU4e+Hu0)Or%@XZ?_^vNqmO!iy+UsRvBy zTwqZuTf4FoiRJ(NZYqy@`RXU#ES_;~Ee}|v+@%P6A9Zah_ZJ%Z&Au&XeRfqwH@~|S z5i8ac%w*U-$>v_u$x*%+x?=VxR#sh0$CfvZIz*e>qnVCYlShVu5Sm2V$m+{K5e0DqRl?s-_(s6fy-H&%Io%W84r@S-q z@|*FY{IAmK)j_i};~oH)2@!4|1i2TH_JWVw0}mA9z`<=%JI*L5BT+%{AvuuRk#LZo ksGkw-Lwci~<1!U$b7VT;L7PfWcmPiDDdYz~1YiUH1L^sjfdBvi diff --git a/app/assets/images/ci_favicons/overlays/favicon_status_manual.png b/app/assets/images/ci_favicons/favicon_status_manual.png similarity index 100% rename from app/assets/images/ci_favicons/overlays/favicon_status_manual.png rename to app/assets/images/ci_favicons/favicon_status_manual.png diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico deleted file mode 100644 index 49b9b232dd1a27e39b1d3c82cbf2292239402d09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb`JYiJx*6vrn-@M8thPoe?a*_mxcKS;hw)Cy^nMXY&X8`0JxQnV%2RuI7kK|yF6 zo6wY4ENv1&nt&jvpaG-J`$3e%Jj}z*rb#y0B)glH9|{>2XPt6c$!VFS0;yG#=aQMnsv-&CPi&EiKh7eJqRo z9_4<5rIF>s?(Xg<_nHfTt*xyd_CLTpYHMrL`}_N8WMqWK$H!@CXoz}xdNhpjoH+ND z9UUEy?nN9}GyA*(-(6i@+T7e6>AKEpds|&yHDcoV36@EY;n@d`f&J8gJvur{iA2H% zb1Z2j)-g#8=AoiUW7_#Q6rF;TtuUs+svX2iwJp zJBnkCrsD6fpvPR5`oD6mM)EC<&Rm}|`0MNIsqlt=aBwi01DwNHEao_Wvj!KP`0lT% z$n~wZZm?^Z|A|J&znU}r;{5E?P=lflQr#1{t2zIDR=%rTtCwr^gu%wzzhF75QTD}+ zn)Ug7j`*3G8RKk=!LhM1BVN8MgeuviPVv29ezo8~eVz8ZZfG+}F4)&L*x*Cz>bv;9 z?@sy=58-gwV6jh{ot>rSIiHSyn%(c7g% zhMnK<7ignLO=7P_?Ten*w z8lIE6LFc_M*Q%52uVVH&JKuF`Ims3=AP?9J_(V_;$TgFyCOaeSP7N z?QE=L>ajqF)rVRzUu&J`^nU1#(DT+ka32p9JIzg{a~B8S*~*_xU~f~+x9ZjWnMl(9 z$aKs)zSBG3D;a9;bX@pwduD?>*_g*&1%aQ0EplldOZQoMZSF7O1ACgT9p)V@j=Usn z4;6(@@5WZlGn$A^s16Og@5ug(zP+qD=MN)HTS-*=KCXNeY($T z;rw0Y*!=J5&Uc!}_N{8+;vJ_ktBy#@p@g)LqSC`GnG}*T$ZLp1P11&xanFVt3oe8Z rOA|{O3peBm-!sTQmZ!=jDaVjYO*u{Ey(DCUyfi@}L&6lM2(8oqz|{oQ diff --git a/app/assets/images/ci_favicons/overlays/favicon_status_not_found.png b/app/assets/images/ci_favicons/favicon_status_not_found.png similarity index 100% rename from app/assets/images/ci_favicons/overlays/favicon_status_not_found.png rename to app/assets/images/ci_favicons/favicon_status_not_found.png diff --git a/app/assets/images/ci_favicons/favicon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico deleted file mode 100644 index 05962f3f148842ece8c1fe9c996abb1692776672..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb_feQXp(6yK(nYm*XCqks~rw(RX57>zOfTNIPh(llbZQ(EH}A;c(z`jrqdG%EfD z9+ci4rGfMVBcO>9HA+%#jR`1!VTkx0dt7@xx!SvOEiHF5`+RS=({6XS?Oo|K?`C#p z-uu0;c`ucuJp3yuk?2=CwN#SsmLy3*!Yye%67A~)&lY-k&p^JbnN<}uvldq%bGb0U zh86|bcwvx*3j(am738lKwZMG~jZ5-eAy$o;R|=XlP#j{s@^cP5zT?pF1BbeE9MqGH z(HZKKL0yh4ZihP;LJzVT?RA34rb2a?(LV-$V+I!b7*J!=fPT zAncdBIi$HGtZnnh7{EOz7(+cqZbb)IGk=q?_g=^n*rZALx}gL4VLq`aYK4`QTQy&D z%_KIbCHaA3LHUqt94N;zulZleoFHwNC-Nba%ez?L4Y#=hEK!enZ5`x{FaFWs{J_-7 z)fB*Ie{0Y`GPNgGl#>)gSAaKU>x04&JWf2P|FpZlE28Q0(67KIj<q9p94}J*cD_Zy%;r}{hmv$~};L~|#0`BRr4g76g&iGUxs7B3&ulRY>>UORzL)%Dx zVtirnsYX#Q&xBvr1b4ZQ@IT~E2J#68%Nq^+K8JYD$9}4lRI8ovJ%_;SKF)6H*d2(! zhrzvjGP?a2OgSbzF&})Y)zmMn_-;S@MDF04j!m}jN7~7tw860dy^HqzqxZP(G=r+M z9K`9NOBy`#F@u)NVn=8Tdab5@LGM}MmjYIeG9Dtpxu{KY`Zbi)9Cj3{@u=4r1&UgHZ-KHt)W?xaipq@p2llqwZFgq)s zH2ItCZbN#@giZE8mjZReRapG~DBOP3`5n%VY@AZh(rw4yO}5KzoKLfx@G;kg&&BMn zj_UYjHT_WJPs0Okx$8hZi?ERn5;hH6%n#VHW%NDSF6e$=OxJ<@K(UxBA1FVmXAw5_ zZt}Z?|yM)5?km%exO*8Ov=f3FLI!_N%eth6xB(p)zmKvgH-D& z=KKwTD`v$HC2}XmA8U;Kx*ePR@2-j2@P!_flf@s53BTIrBE-Jq7}J&p;9-Gjo#ha} zCco{BEx(HPnS4Lfx^#C&uTjh2VXzGIXT@Fyt0;%@+Z|+4E2jBd*)e)dsf~jpJ)8Pj z^(9*8BBlQNFOK{uSKdC_WX7|u*VLyx>V}BsWSggcxYByx>DyD6^g8jYPee53g|QMR z9`l}B9ZT5wEBj~cPi9`RRyX#gtumDASX`XXHY>G*pW1kz`tSSMc&umY=J-!m8EWld zr}duG_u6NYk6L5k*_!k@-JNOA<4e3W9Y3N`Y-hS}-B&jaq$c?{M$^{wxxTT&=BX9Y zjF91}{x>lfkNMnP8~;l1k}u6?*{+o*uU!%{D5lx+(Y-ak)MJ8otv9(X7hZ7}W3bDR zfxfRBUMKdmot3KS4W*)={+r9omdAY0>xF!^qK~VaVkz@}wrh3E;2CA}V0X6no$ew3 zd3DQByVIF<4?rphOLfvrWKbi{$jFeWAS97`kQ$M2ke{fZ5iLPl e)hJ2jI_>Kz?}6TZg2K=XVMyvU48xFuoA5sgl%aC~ diff --git a/app/assets/images/ci_favicons/overlays/favicon_status_pending.png b/app/assets/images/ci_favicons/favicon_status_pending.png similarity index 100% rename from app/assets/images/ci_favicons/overlays/favicon_status_pending.png rename to app/assets/images/ci_favicons/favicon_status_pending.png diff --git a/app/assets/images/ci_favicons/favicon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico deleted file mode 100644 index 7fa3d4d48d43f7ad2a4d76dd51edda2a863904c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb`IeN0|%ZtZ*BjQ(Lv_7_4ijDZA%l5K2$Y|a^V6WpdUMR3Uy zGg~@9Ajr1xF%xAUe{6u?fD(x&&P;I|Er@)ULJKV`jE}z8<2e`J%j@ea6vrcndwcIa z=XdV8=ib9{arl>zz|nv1%Lh2_VUFW?6ddB7MWJ!_;oL@l&}@l!*phZStVy-Kw&b2( zTgtSV9=2=x7| zUJ4Wkb{3cq>pcg`ab!qKzA?q-!el@6Rb_>7)jm*v;>U7jJ#{D_C@0r*FQB9JK8%~s z)p9b-mcOTsa(`7m`Jj#i1_|=(~m{(zcAQ%a8)1ZEY$fx zcaz?SSolwrLE;&!4kU}TQ zFEG2(LO`FtGD-}*XSzPny^ue8qbQf7@i$k%L&i$UX=oC`a9)JM;>*ltFyN6*L~Vz^ zD$wNz`nnVydMD|v*5jMbfW>%DTx9G<-ij8O;JwEOg)V|YOP4z;{ zH`>LEhDIsG*tGXkD2GMZUNFJ<-tLBISY=#~uf8|*R#Uy8`z-SxuY%3S8gX3V61#kv zO?;A;j}Acp*?nPotUy&AEuV5^fu?sQ7O#!E1Y) znV$czR>hWHN6jB>I>hd$vJUSF)hw!;RL6|v;#I>1RlI56^(gPE*tGu_10a}(p|yBF zILgzb-$Mks+D^g9#iQW+<4Q!VsAkdLsdm%e4fT=~wr}E>YS_p(X1EwX@%iiU`zM*; ztlhP$56XU39jaL?akmkhzKAooMzReUd)m7!S-I`Z2Z{qb3r(FP+r?UC|JBGo63I8dGOMslYgro1<1x+D0qAC!|VC9g6+$QP|#w`W2{l{IwQUiFmB z)V78iY0~h4KHKnCFe+R2f!ghFyV)C6c;Ye?efR~uUi39&9lHTS@e&xTMVFST{>>kn z`<&15fUfKf{3+!5>edoI|ISTaeY9&hI9H+OY5QCA117;d5>oF+?wj&w(zNUKew+6A zEwWBnn=%sOU!G6U(@~EJ8E#*g9_4%5Sk(HY)|LChj?pEp4L-x|k>^)y`5f=12;CQ^ zep*dOJ11m&e$(0za=cC2b$Y+I?eagZJpXa3NOc8e8>Wm2-Ad1H>JRo10gs^?)EDBg-~0_IqZYk;h|g`JJx2eCn7;v5Zwmz0P8Rcp-IE64KoP^?0m( zA=lf?XL~zhU9Yze^}B`K=?1+y?HB-;2qA70EO7Uu#DkxUgFz-7baKm5iY+RNDD)us sQ94mdQShTUQ9UEsgz{J^$0af;=}hc|!4g@9U=W7D&twK>0IL-I2d77AhX4Qo diff --git a/app/assets/images/ci_favicons/overlays/favicon_status_running.png b/app/assets/images/ci_favicons/favicon_status_running.png similarity index 100% rename from app/assets/images/ci_favicons/overlays/favicon_status_running.png rename to app/assets/images/ci_favicons/favicon_status_running.png diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico deleted file mode 100644 index b0c26b62068063c7d9a343be7d5d2feef25a4c42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb_eZ%kWN6n_L0KhDJHC!-{8ukUp;`oZwcpw5I18sp?o+%USi7_-bROg9r_;)sbR zCfjh3vEbse0VZaUn3$-El86opT?~q4!fRgNdxh3<$*=d_ zKj-|;pL@@RPbHH{X2W(I3eLsAz`#?;opih1`8jQ34#k0TfqI$? z%>&A{;+c~-HZ}~J?1CMk!A3;D+2m%hPQNhTBu8FWgL1S2CO85>fm6buaw?PvqtO|-c? zAO7dALD}ifzy|S??Pje|8>l`Y|Lku3LZjSt;I6D(=<>0c5l^m0YnW5!cQ_mdiUXcm zHrAdGlBXJl{v>np?{9)9EY0%2e7gkvZ3)iYm}mT(o13}l!SDC8J;TGpkWQyzW@d(s zM@L5u^=bPIuA1>%zkwRdchV-~ZbSZ0B{=?dkm;iPW7Z1cCV8sW82?c{zok*?>mJt=iFI<6h2X0GJ7WXaWtk>|LO~JeM z6HJco`~PiJo9N9#-`%LA*m7O^iSN+FoBY0m_(a1^{=X9g@vslRzxM#__Wqpra5uZ? z%_86F?Iz#(JF=YRH}N;u1H~bqT+n+#Z`L+<8^wK#9dq9(wPyTpLGJTLe#@zNhFi6P;y}3|opdJY zUeMj7`apGv>Lk@_I)}aZHvDzJPr{gARC(22XoM2J9rKShhX2^eP3PY7dceT1+CgWs zqsztYAY1hK{r&)a^{Yo|_;Kb1l~dpIH{~GznK?KAVLB(bY6JD97Za>TooSkf`W7FY zx)uQ27hw=TNBdi(1wE&BEPRmoR;UVrDc+8ah_&HcyFsX1`^Qus_44m0nzVfS?}K$Q zt9W=$(fXr)TkE1n_501n))SE$RVJ%{ZB7wRZj_kGXy-&*Ae<#f=l$q>&i6?xtA2|| z{3*Q-!4?Rq>pS&A)pD0X_RW<)caqVsiN`_?y$-Qzxm~}{Y<%dg$cy?MSdT|)&GzP+ zv(|>+%jTa`D7U%x>*vMF#aKrE$U;iLzS|hEX7j0 zmhZRlM(}S{2jw(hIofxyHuj1td#EOIx)51)FXrIliD;=hpKmWzEUXLV^Yl+% zQNA+TeKj>uESCFau`&?T&gc6rRxjTZjxGO@?|!p=C~p_5SMQpw>E{5rGElgEkmMdk z6hnk70xtsxJ=_)=utz}&K?Na#=s`3fuu+`oJtOQxJk!8&Wek^l%6h4+XxT< diff --git a/app/assets/images/ci_favicons/overlays/favicon_status_skipped.png b/app/assets/images/ci_favicons/favicon_status_skipped.png similarity index 100% rename from app/assets/images/ci_favicons/overlays/favicon_status_skipped.png rename to app/assets/images/ci_favicons/favicon_status_skipped.png diff --git a/app/assets/images/ci_favicons/favicon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico deleted file mode 100644 index b150960b5befbb2a8ae5a61cdcb64437ac9a98d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 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 MwB#vFA%hwG2hr)3h5!Hn diff --git a/app/assets/images/ci_favicons/overlays/favicon_status_warning.png b/app/assets/images/ci_favicons/favicon_status_warning.png similarity index 100% rename from app/assets/images/ci_favicons/overlays/favicon_status_warning.png rename to app/assets/images/ci_favicons/favicon_status_warning.png diff --git a/app/assets/images/favicon-blue.png b/app/assets/images/favicon-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..2229fe79462352e45e86a52c6c969647db689596 GIT binary patch literal 1522 zcmZ`&X;4#F6ux;`2??@`AwUT6z5AXlVNqE`r3M5shyfL(rC}EpSw&Q&5m19-L0$OE8+^U0sh;gHe5c@)AZ2Z|D{c+EC&b{}$^UXQ;tB8(T&Smr1 z003@8xI|_`!MDYtnxdp>|1A^HGD4&w0GvN$ZcL(oJ4^|eNdee_0-z`dz+l=^=m6L% z2H>q6fWTS+X6MvjS-l7VBq@4jY^do1UtHnrSM4aSbn-bwp-|AsAxt_>p&$TIu$7~j z^rb21Sb1331#oSAEo}W}<3&8rJ#*Yk=D7dlwyZ}Kc3%|QCkpLnvg|P#X~uD3v3X|Z zj&^QxySbURZYg%6beF&u*Tt6vi_W(rs)h0jb?^Lf5M_CO^M|ki4}j9 zB`?B?7wPC-EX7Xg& zb4%LL(hjDDHK7?I8i&oS?d^hPc1z;~n`>Ni>hPL-?&^nbTWZ{L>kx$Mw0?(6q5`4P zZ2co~^<%vLiGA>Djx&ZG=z^*qjtfpVw-Dwxhzc9&oLT&s9K3PZY28)|g4p;h!)qVl zbq{U)!q^T1Y~K~E;u@PLq_Nn-tZHF)EuGEbMWzuahj7gcUR0*#ya1x+K7MS#+G{b( zmWP#eV2a<^_D)nfWA0j&XzdXygN4S-pfN^NHiOCx_>O68n*rahC)DF;#SB_ugjpoq zOcHhD_^DB%`aLWmVKGS@{eU-)z=Kn$at4btVCxL<;52dKCAOc08%UT=Vig9WVT`DK z1B*ylND{~2;D4SJ6_uALdItLM+8dpDyu^*E z>1R5%TE<5`hcz|DmPCiIsH(3%hCCl19z}@XwVmR^;v2oT2BX9J(ln#AVQEiIw; z{f9Rp$o5z|Ti_iEmz(vh4qKx^tYh|Q9}b4_xzg0_9$8hjD$m+q9@7|xUN%RNyjQNW zDo?3w?V<_O*~#jkm-*W(6)D3P%a!r*)PMzTS(frmOr}1%v5=`jZmaHjigjB`%bbKgnWhc$4WoHv!jN$T$!AvpvCZ-$lMZa9$DK64aPybm@x3A^K zh?#dAf@Z;2H;1{RnenoQ^PpMo8rLnXs+?8krL4Q^Ai9u>OG=*RS!Ul%t*Ue>TcUiM zaG5RY?cvpen+b{=2_fuGN^k%@b#np7U6=AwQ$lBs>)YGogA~XoGiC2c#LBnto;`hf z$-GIvZQX0*_3OcXIK<)nw(QBF_Lc|gI=%G6`bC$$f~moF8R^q)&tGf>wv}fs^l7i< z>6e~g?b~PJLSdw2+#WL`4EL^9txk6&?ar|;vyW?i@>Vf;Tl#Ex_8&r4c4}t&_Wvg&We;SS2p}RfO49zFy!2m?`$ob5 literal 0 HcmV?d00001 diff --git a/app/assets/images/favicon-yellow.ico b/app/assets/images/favicon-yellow.ico deleted file mode 100644 index b650f277fb66d6193e27e1f2bfe59f751159270b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5430 zcmb_geM}VT8GrRIB{5da#a7(eot^!faeIy0=I+uqHTp->f7)CvX^b_ExtJJeqvmSW zhW%g>?@J3xv3K>WD6onWRspw&7A(259-_!e!6;oR7^MwRZuvM4cLv`+&&Pc#Tw?{U6^#nzqX;)X|51 zd`+}5%eu;>=lGfL*ed7QksjfSNL}Y=sJ(iea({ZL#ap3{KI}t`vv%Z;Bk$OEkz?sw zht>Ug^ z=Y|%KbvyWw!vPqfCC+hCoDdqsTNv*QeJPI-F}A}aTCBPT+@FUPZk$JGS{Xxw*qyVj zqLBaVV=Wfr*#q~J;@Q&sN<7X3IQZ~mnzOy4c=?9HH|)noD0k$)npt*VyZMbR)d}X< z^^co+j16LP))juw?RFE!-#M&~lPT;RTd>4EU%x+CUjjbBjp}ftUuP7YX=le#&Ak`x# zseWUbwQS`=z#uO6HK^M6(GQ$ReceB|?hksl8B#Dz@JrRIF=~5Y;GE@(c%y1($--vl|oSKXO33i)0@Z8mlF?VP-;y5(MeHHFZF|N{~H-!t*knlHA za#TVdJvI)xpk2-f_9Kvgs-`G^S){@RamZ0Rl$(j~dHQW7kq?AsX&CdgE2!jQ+reQi z7ugO#4udst2EUYZIb!Xlk&@@pNQM2*-rj{=Nq1PdERJD*hx-b2lswM9_b*MKeK(%R zaUVjEg8>UMQCmrO^S6q66V7ugom4)XzjMW_A+a5JW-yP#T>^Pl(YBw!weQgMv5r2> z(N4S*F9IH8Al4m{+HFOh@|~1QCza1q@X02jQTj}15ha-i@3vDkUZU=1KfjhDHarGB&La@m@~)|6+|{B}5J%T?(#+`FS%9FD&+2Ip(?DL34=mOWib z`b=>bwO1;8Am7IjBQ!pp19HN?jJ{SkdnV_1;e_`LCco~7!g(poUyAQF>Y~AUa-M>- z2G138%68=QByIKXu^;~YDGu-sh((Ts`G5vrFEug0sauO{zbvnMqtGyW74|6+2l}qF z%30I2aqUb0lUAQA8A{N{al6jIv!KpLEm5ms_S&*7m79bRJZE@b)V_GhpNF$nrCay- z{4MX?Nvm&pr#Dd_*8rXy{+#YvP<2o~NGmDk`e#&f4U`|A3e zimp>TVD#&Sbw@S4n>8u#W_m5is`J~&~`681bk23z@P+PSM^u$7G@=_~sH z?{`@x<&7y}m01d#*cc`nj<0_h-~+--{&c|(yZl!^wqtiM;HIpe|#?XqvGMC>>nSb>>SrC0(oP2ZKRk_H{*#>!`LF|E&pkZZPHdoywsvb9009nP>8KO}w!!m~aoUo_j5- zp0Q*-cQd|!>-Dj2@!oAS@~Ca-OL|Xd3m$G(`;Dc|JFvsZpQ+9H2l9t10BZ^Sg7_&2u+d`mZ;dnHvo>$`K;h_`rq z`%_`(;xl`DBSt>6w diff --git a/app/assets/images/favicon-yellow.png b/app/assets/images/favicon-yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..2d5289818b47e3da5d8902302d01024acd26c7d8 GIT binary patch literal 1667 zcmZ`&Yfuwc6kd^PQK?i#F`MTm*%c8D4;=+M6$eDBPz?AeDhd?QR*Pb^LJ|l8Q4tW) zBG6PN1c*X-iGWDZk*81+pdb>F0zz6T#Dr(uT_#SfC&zyVR&)3Ux z8EF{+0G8f+Jh%kY7uM&CiRd9qY9h!y+}+0=0LpGI$AdpxxD4IH^#Oor7yv{G0Duxl zqQ3#)A_oA*P5^*QCIGBEC#l)*1^{Nwe)|GEiGtbLS(!|xR4V(TX&OFN6TJYHkoEV;R`Q-BN*0#lhtudrWLFL8ynD5MWX z)80f=-|#5})B>_f2+Fv&GA>sw0M*3)>^9R&{&>8daG%wf*u#{w1J6OUN%Ya{bX`m#QUwmo+9!4*UaBX2;c<>RsiE=2v!c`3ItOix#8>Q%Mb!NVe5-dfp`gmJ%tGg zUJ4ie09KqM;|*b06@pd4^F_`T7i?;y$ap1;S0gpiWM$0P^-&bO9KmW4{4r85ptcJc zn8FULL2!j_yNJ;f@6eS@CsG1k$&4P+#{Ogmq3utBv?**tf;~g9$A~tCsk^xWFW*YI z!b;(Jg{|&3bLKt^Er8KHcmM-nInb*ZlKuf?e8|RX;F)~3K?dnEAl)yFnLO5lndb;v0Z&Vrh6j); z4s4GDr=(2uF>F+@C#BH9Rce1C)gXn4N6pDuhFoYoo$*f^V^+?_D&c7v%kUf2Dk4qi zGSNx|t3%KdcuESrPN4K8P$s3!PsQ+GDRgZzeNN8)m<4^zgl6)XXgM;K%T&iv)MC(( z17SqRl*p%t@X$5-&~@5mE;N_N(%pt8ZZoiIWKsqh?n9HgETZRVG5j`>+INMDDqy3G zJ$92Zmdcouv1W3ZhF_r;5y_AR{UIiIiphptruGJXG>L99nf7)jejr9UnaB0@Cm5)F zQB!Ng&5Wq;(SwIeYb&eA^*SBwylHb%W?K52f&Ts#M$~2%imnQYJawk&MT36Q+)O`d z=8`SR-kFW#(!mib4o3=FRg<$FU4acvL2eaBW93ep{PA%)DY<_j%`B(`?+V%6Vr+3- zQK8dUuedgr?5oAKT&e~|`{@-ZT77BOD6ng_Jwn)76tdULW1R8yF-Rm!dEP#rONY%Z zEZ5Q!GrLw1UySqp|J+7rQ$4VEIM^jFm zaO(wl6HS*l*t}85O$l=<{p)vggE)t;o|C9maH!1ev#hJjthZ%Bp31M5H74=bU$6l0 z@YH+z&g5D8FL}|d^jrHt65P5t;M|q~bNNWNo!8=qfHLiY(6j?>4n2zw)CbDFiw>qf z^vO?s;&=D(S6h1{cIF2&FAP*$l;yX}PJh9*|NTlv(v$QiudLVFU0|c8OH%TiRwSlwSQYWH z{_vl`d1rS`LufI6S!#~>CW&H-o?Ag5?wA)GyW-S!&*$YS0K8WCUb1_nw(wk>`5cfz^l0C2F}|GL_6@NSscp9D`KjJKT4vyN$~?){X?`Zx(A+PR|EEEf+Ty_PQ;P zaNc_3d6b=a-RAO`kQ;Xs{{HA_YRcEWxWB0uE8m&v}#)S literal 0 HcmV?d00001 diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico deleted file mode 100644 index 3479cbbb46f60e2a0d1b830d07e4b9cdaf8180db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5430 zcmd5=-D_M$6u&Vpq4;1$B!p5-DVclM7ykfXl5SEgVq<6`NGWSS(%8m)HK46&O^a2j z!PpkmwIY(E}~_!o%yAbF4?!6zy0W{qL4^pQ+`x%oOJ5otkonvvk7WkZvS$Vy4$dv8>|L9)uON%rzhZuO8QX)H%d_vGH)uQTNX`mJW_Wl{ zlCz_$dB`lxIYK9Hy(Kv?u46GPftCxLhF+h|^{yq?9vpfn)<7|wfqhJHknSDe8DJV9 zUd}GUKWqVq;uij4Z|wb3aEiH$;CFqI%^$#zVtfU1@j&`)2RL?v4t`5|}UQ z^<+#ovDQR+M2_!;_fW0{re$u`7F@N^`z1&ZbE5zUC}+c}o!ZCT1x0T}&Ox>dAX`FM z%Ka_ob^zpuP@;$Nec*S$jekr!L3-gx@_ooDzFNPHI>!76Fs90=57|y&hhOd=L#IL2 z(=^T>2UKSofo4F~ne(DHL7@Ej0>Ko3@um8%U1ZR)n$`*h;-S+`s@1HtoY_x z!hWK0uu12dek7OEqbnv$SU<4qPsTs$LIb;PFR*j#R(RiErqMocvJXVfNTYV_#=OW) zwYjWD51b;mXnO(9KCzeC$wxfgn&RP>Bx?G)!!Fq?*aNCvC&b?Ti*bN61l$80)I^h= zu(*!eFKSLplIzjGbD~Cmi8-%+R+B8k8*R0?+fTtP92fk_pm;?M+UL?%@2!r3Eic=xC8oW4lyR6A4BWf zep=2?keqx2+kmuZPyRXk(DO}qXf$im{VUc_IlvyU?p0IVb99fLw=>>ZaBbuEEK5A1 zJuLf;V$S`h<95{Z>&8C`zH#vNqIK_JZnUTK;_S(qkNXL-48y(%9n!sAI14fcY7fzx zgW|wjf!2M)^X=aIbe6sakK&v7@dha7UdGV;5czlHJAuQ-!MlXcfa;(6-vcGzD8}`` z3Q(#```BN2dx*8^$JcV$`(o~;f3*7BS$7Y-Jh1`T-@rL=zX_E)17Gwes{;;se&u;_ z^(gPBv35FBl5cRGhxey*g?(3i(|#Bo3&r(I8{)X-TUWerHizLm=lL(jN4`J9p38TW zGXoD%X;A%}7d+tllBWeLV zzJ@oq_a>J1vOS9!-1Yuk@E@}evn!|_RKux;_<^+>9rLL5;(ZzT?G<96TGYA7+0gn% z9^xe*<4y+OL?659gp7`~sEOdWaF=HSumag~_|@4c{CC06fz4pFrXsLGO$_8CA9=47s_ZNc|b0GG=dnORVIu%24r9N+*!`R!BnH|v9= z4mezaugA{;-FhI%H&n$tzK|=wt$z$2S`WSZiBDaV>^}>6$sOSNNO?9g4yrd(e*S95 zu$}YqDMy4B2qi~g>pyp#XrW;W0n^K?mnOXog_8yuGuzBA}o+{>5t zCe@tL-=uXxyw%EJ|D59gW%M_kdp{R+rh$?g;n&&97+enkn3VsYnw*0keY?ZJW9ObY UJ-#T~Ug_ii)GGf9_)oh30I9}9&;S4c diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..845e0ec34a513a2c35702b4a2390f33f1732d6e2 GIT binary patch literal 1611 zcmah|4^WJ082{R}Hjy~Gm9wc)@9tu?Wt7QvZfjE;rGFg4s%>+rt*}ZZDMXnq#juE^ zltaj$F|IResMA!}#Toh2X?M#?Y1Otq+xLET@7KwlnQm_8`QAUj_j#V*^ZefT%MJ|i zv9y?Jfnk`X*jE&cW;^OLr=w9+np20SahrwyLJX_Sn+U@h)S=Wj*dN1E0EX?^hhc*V z+Vcj(b_g)+?IsL!S78_{q44LRZ!wIP5V#`Ldw6(QC4lMWYdZ$q({c>4&%aefXoN59FS!ImH?Oza2fa*1>mlcEkcl$ z0=N|*Mj{r#Z6ioAfO7C|6}KyZ3yVfQkAe?U4j##Ys!;^R=1`u97w#PacY>|}UUv|e zO8d8+2QUsXat1bWVEiap0*G@X>q4OQ=8jpnGr=I7)k)`-}6u&m;g|}pQ+yla&^mj5H{SwY6Bnzm8ydlJ%J>qi# z45fkBVjfbD(ybI2BRG8$4uOcyQYsCpz`!mJJP(Ndj{2=!>W(}qXYw#WEs*h4CFn+w z8a<)hp{)QS%@MntDv#1Oj%t>oM$Mu+w@^8?NV;->ZWC2PWsU(vG#ErjDF8h-^VsGp z^eC??f&)U#=-cIzn8)3(m^RF*PK-m@$$H}56O)=v&Sp-NrAza*^}05eRa*GwdVE}IG11m;RW~U^zZF|75{52rP=?4- zrwON=X%93H=34f~)o#c;$DeL7PF|^*T$B5C62G_CCBtF*$(Z{N4!_&o%RbSxf7v?Q zKb>x0?P=_N$*vx*4laqhzWP$)q)Q=g$v=2pUjNi5_HaSFn~Yf0^_EC{ zBQ*BRj34u}_)X^*wMBWwT5k>un^U5!f8OtY>$Ky7mW?#_ibp}T)0!oUD1OEA$Tn+h zyRv^qk%tLi=Gx5?*DNzFTIJGG)b;%>?ko3z2Y#5cOo!zSYaGn>qez2mKHX4YQA z{TgO`7vWHS!D_+nYD48X|HVtwRu{w!s46k7-><2bHT$mL%gFBZJ5lOC{^1`tc_C?@ z9*u6APHgg?mDzRwoGe4Deh2TX~qpo?vl%|Y3v&=jEj`Sy&Nn9lw3G13vVt(kO zu%@CR*GmfrNV=t!S-YBlZhI(~iTqWM>pFQx3`VMVpf zvzFhu^<8j{w^!1_$imjL+-q%u#Ga!Ys%CAuJh`>$R%YtrO1B4ItvXjo$kJ`!yzVg4 z_ctDzq-{@nkmI#cuNJiAm!x!7wWw!AtF?96M-GcC{(Z_I7i+gJic z%Z{H^IZm8mX70S_Pl}!vT0d33tX5v!?Zl3IR3RyTk<^=|&!Arvy(XDfv&jX!)+_lt z`v;yGSWjNO|7zpiEVuVgzoZ?vgJwxWSKAY7Y4_3Faa4G(QACC-WbV>!GBjZ>0%teA zU=iP05bEsgzEI%4&}E*$#a$q1>@BbTB0wG=86B1U-vM`T%uYrDnAkf&bj5R1=3lz0 B24(;N literal 0 HcmV?d00001 diff --git a/app/assets/javascripts/favicon_admin.js b/app/assets/javascripts/favicon_admin.js index 6b2dcf4502e..97e87054ce0 100644 --- a/app/assets/javascripts/favicon_admin.js +++ b/app/assets/javascripts/favicon_admin.js @@ -4,7 +4,7 @@ export default class FaviconAdmin { constructor() { const faviconContainer = $('.js-favicons'); const faviconUrl = faviconContainer.data('favicon'); - const overlayUrls = faviconContainer.data('status-overlays'); + const overlayUrls = faviconContainer.data('status-overlays') || []; overlayUrls.forEach((statusOverlay) => { createOverlayIcon(faviconUrl, statusOverlay).then((faviconWithOverlayUrl) => { diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js index 9ffbaae3ea5..9bca1993ccc 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js +++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js @@ -513,7 +513,7 @@ const fileNameIcons = { 'credits.md': 'credits', 'credits.md.rendered': 'credits', '.flowconfig': 'flow', - 'favicon.ico': 'favicon', + 'favicon.png': 'favicon', 'karma.conf.js': 'karma', 'karma.conf.ts': 'karma', 'karma.conf.coffee': 'karma', diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index eb3261c902f..2edfe7d81f8 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -265,7 +265,7 @@ class JiraService < IssueTrackerService title: title, status: status, icon: { - title: 'GitLab', url16x16: asset_url('favicon.ico', host: gitlab_config.url) + title: 'GitLab', url16x16: asset_url('favicon.png', host: gitlab_config.url) } } } diff --git a/doc/user/reserved_names.md b/doc/user/reserved_names.md index 50ec99be48b..6c1378560ef 100644 --- a/doc/user/reserved_names.md +++ b/doc/user/reserved_names.md @@ -58,7 +58,7 @@ Currently the following names are reserved as top level groups: - dashboard - deploy.html - explore -- favicon.ico +- favicon.png - groups - header_logo_dark.png - header_logo_light.png diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb index e28d4c67661..d0178d0fdf9 100644 --- a/lib/gitlab/favicon.rb +++ b/lib/gitlab/favicon.rb @@ -3,16 +3,15 @@ module Gitlab class << self def main return custom_favicon_url(appearance_favicon.favicon_main.url) if appearance_favicon.exists? - return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY']) - return 'favicon-blue.ico' if Rails.env.development? + return ActionController::Base.helpers.image_path('favicon-yellow.png') if Gitlab::Utils.to_boolean(ENV['CANARY']) + return ActionController::Base.helpers.image_path('favicon-blue.png') if Rails.env.development? - 'favicon.ico' + ActionController::Base.helpers.image_path('favicon.png') end def status_overlay(status_name) path = File.join( 'ci_favicons', - 'overlays', "#{status_name}.png" ) @@ -27,7 +26,7 @@ module Gitlab def available_status_names @available_status_names ||= begin - Dir.glob(Rails.root.join('app', 'assets', 'images', 'ci_favicons', 'overlays', "*.png")) + Dir.glob(Rails.root.join('app', 'assets', 'images', 'ci_favicons', '*.png')) .map { |file| File.basename(file, '.png') } .sort end diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index 4dc38aae61e..e5191f5c7f9 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -30,7 +30,7 @@ module Gitlab dashboard deploy.html explore - favicon.ico + favicon.png files groups health_check diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 3479cbbb46f60e2a0d1b830d07e4b9cdaf8180db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5430 zcmd5=-D_M$6u&Vpq4;1$B!p5-DVclM7ykfXl5SEgVq<6`NGWSS(%8m)HK46&O^a2j z!PpkmwIY(E}~_!o%yAbF4?!6zy0W{qL4^pQ+`x%oOJ5otkonvvk7WkZvS$Vy4$dv8>|L9)uON%rzhZuO8QX)H%d_vGH)uQTNX`mJW_Wl{ zlCz_$dB`lxIYK9Hy(Kv?u46GPftCxLhF+h|^{yq?9vpfn)<7|wfqhJHknSDe8DJV9 zUd}GUKWqVq;uij4Z|wb3aEiH$;CFqI%^$#zVtfU1@j&`)2RL?v4t`5|}UQ z^<+#ovDQR+M2_!;_fW0{re$u`7F@N^`z1&ZbE5zUC}+c}o!ZCT1x0T}&Ox>dAX`FM z%Ka_ob^zpuP@;$Nec*S$jekr!L3-gx@_ooDzFNPHI>!76Fs90=57|y&hhOd=L#IL2 z(=^T>2UKSofo4F~ne(DHL7@Ej0>Ko3@um8%U1ZR)n$`*h;-S+`s@1HtoY_x z!hWK0uu12dek7OEqbnv$SU<4qPsTs$LIb;PFR*j#R(RiErqMocvJXVfNTYV_#=OW) zwYjWD51b;mXnO(9KCzeC$wxfgn&RP>Bx?G)!!Fq?*aNCvC&b?Ti*bN61l$80)I^h= zu(*!eFKSLplIzjGbD~Cmi8-%+R+B8k8*R0?+fTtP92fk_pm;?M+UL?%@2!r3Eic=xC8oW4lyR6A4BWf zep=2?keqx2+kmuZPyRXk(DO}qXf$im{VUc_IlvyU?p0IVb99fLw=>>ZaBbuEEK5A1 zJuLf;V$S`h<95{Z>&8C`zH#vNqIK_JZnUTK;_S(qkNXL-48y(%9n!sAI14fcY7fzx zgW|wjf!2M)^X=aIbe6sakK&v7@dha7UdGV;5czlHJAuQ-!MlXcfa;(6-vcGzD8}`` z3Q(#```BN2dx*8^$JcV$`(o~;f3*7BS$7Y-Jh1`T-@rL=zX_E)17Gwes{;;se&u;_ z^(gPBv35FBl5cRGhxey*g?(3i(|#Bo3&r(I8{)X-TUWerHizLm=lL(jN4`J9p38TW zGXoD%X;A%}7d+tllBWeLV zzJ@oq_a>J1vOS9!-1Yuk@E@}evn!|_RKux;_<^+>9rLL5;(ZzT?G<96TGYA7+0gn% z9^xe*<4y+OL?659gp7`~sEOdWaF=HSumag~_|@4c{CC06fz4pFrXsLGO$_8CA9=47s_ZNc|b0GG=dnORVIu%24r9N+*!`R!BnH|v9= z4mezaugA{;-FhI%H&n$tzK|=wt$z$2S`WSZiBDaV>^}>6$sOSNNO?9g4yrd(e*S95 zu$}YqDMy4B2qi~g>pyp#XrW;W0n^K?mnOXog_8yuGuzBA}o+{>5t zCe@tL-=uXxyw%EJ|D59gW%M_kdp{R+rh$?g;n&&97+enkn3VsYnw*0keY?ZJW9ObY UJ-#T~Ug_ii)GGf9_)oh30I9}9&;S4c diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..845e0ec34a513a2c35702b4a2390f33f1732d6e2 GIT binary patch literal 1611 zcmah|4^WJ082{R}Hjy~Gm9wc)@9tu?Wt7QvZfjE;rGFg4s%>+rt*}ZZDMXnq#juE^ zltaj$F|IResMA!}#Toh2X?M#?Y1Otq+xLET@7KwlnQm_8`QAUj_j#V*^ZefT%MJ|i zv9y?Jfnk`X*jE&cW;^OLr=w9+np20SahrwyLJX_Sn+U@h)S=Wj*dN1E0EX?^hhc*V z+Vcj(b_g)+?IsL!S78_{q44LRZ!wIP5V#`Ldw6(QC4lMWYdZ$q({c>4&%aefXoN59FS!ImH?Oza2fa*1>mlcEkcl$ z0=N|*Mj{r#Z6ioAfO7C|6}KyZ3yVfQkAe?U4j##Ys!;^R=1`u97w#PacY>|}UUv|e zO8d8+2QUsXat1bWVEiap0*G@X>q4OQ=8jpnGr=I7)k)`-}6u&m;g|}pQ+yla&^mj5H{SwY6Bnzm8ydlJ%J>qi# z45fkBVjfbD(ybI2BRG8$4uOcyQYsCpz`!mJJP(Ndj{2=!>W(}qXYw#WEs*h4CFn+w z8a<)hp{)QS%@MntDv#1Oj%t>oM$Mu+w@^8?NV;->ZWC2PWsU(vG#ErjDF8h-^VsGp z^eC??f&)U#=-cIzn8)3(m^RF*PK-m@$$H}56O)=v&Sp-NrAza*^}05eRa*GwdVE}IG11m;RW~U^zZF|75{52rP=?4- zrwON=X%93H=34f~)o#c;$DeL7PF|^*T$B5C62G_CCBtF*$(Z{N4!_&o%RbSxf7v?Q zKb>x0?P=_N$*vx*4laqhzWP$)q)Q=g$v=2pUjNi5_HaSFn~Yf0^_EC{ zBQ*BRj34u}_)X^*wMBWwT5k>un^U5!f8OtY>$Ky7mW?#_ibp}T)0!oUD1OEA$Tn+h zyRv^qk%tLi=Gx5?*DNzFTIJGG)b;%>?ko3z2Y#5cOo!zSYaGn>qez2mKHX4YQA z{TgO`7vWHS!D_+nYD48X|HVtwRu{w!s46k7-><2bHT$mL%gFBZJ5lOC{^1`tc_C?@ z9*u6APHgg?mDzRwoGe4Deh2TX~qpo?vl%|Y3v&=jEj`Sy&Nn9lw3G13vVt(kO zu%@CR*GmfrNV=t!S-YBlZhI(~iTqWM>pFQx3`VMVpf zvzFhu^<8j{w^!1_$imjL+-q%u#Ga!Ys%CAuJh`>$R%YtrO1B4ItvXjo$kJ`!yzVg4 z_ctDzq-{@nkmI#cuNJiAm!x!7wWw!AtF?96M-GcC{(Z_I7i+gJic z%Z{H^IZm8mX70S_Pl}!vT0d33tX5v!?Zl3IR3RyTk<^=|&!Arvy(XDfv&jX!)+_lt z`v;yGSWjNO|7zpiEVuVgzoZ?vgJwxWSKAY7Y4_3Faa4G(QACC-WbV>!GBjZ>0%teA zU=iP05bEsgzEI%4&}E*$#a$q1>@BbTB0wG=86B1U-vM`T%uYrDnAkf&bj5R1=3lz0 B24(;N literal 0 HcmV?d00001 diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index a08fcea27a5..06c8a432561 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -265,7 +265,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon - expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico" + expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.png" end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 6e8de6db9c3..eb83884d54e 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -681,7 +681,7 @@ describe Projects::MergeRequestsController do expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon - expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico" + expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.png" end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 9e7bc20a6d1..c524f5d7f8a 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -190,7 +190,7 @@ describe Projects::PipelinesController do expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon - expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") + expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png") end end diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb index 7c4fd25bb39..25c408516d1 100644 --- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb +++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb @@ -12,7 +12,7 @@ feature 'Merge request > User creates image diff notes', :js do # Stub helper to return any blob file as image from public app folder. # This is necessary to run this specs since we don't display repo images in capybara. allow_any_instance_of(DiffHelper).to receive(:diff_file_blob_raw_url).and_return('/apple-touch-icon.png') - allow_any_instance_of(DiffHelper).to receive(:diff_file_old_blob_raw_url).and_return('/favicon.ico') + allow_any_instance_of(DiffHelper).to receive(:diff_file_old_blob_raw_url).and_return('/favicon.png') end context 'create commit diff notes' do diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js index 25ca8eb6c0b..dd025255bd1 100644 --- a/spec/javascripts/jobs/mock_data.js +++ b/spec/javascripts/jobs/mock_data.js @@ -20,7 +20,7 @@ export default { group: 'success', has_details: true, details_path: '/root/ci-mock/-/jobs/4757', - favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + favicon: '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -78,7 +78,7 @@ export default { group: 'success', has_details: true, details_path: '/root/ci-mock/pipelines/140', - favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + favicon: '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', }, duration: 6, finished_at: '2017-06-01T17:32:00.042Z', diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js index 70eba98e939..9e25a4b3fed 100644 --- a/spec/javascripts/pipelines/graph/mock_data.js +++ b/spec/javascripts/pipelines/graph/mock_data.js @@ -20,7 +20,7 @@ export default { has_details: true, details_path: '/root/ci-mock/pipelines/123', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', }, duration: 9, finished_at: '2017-04-19T14:30:27.542Z', @@ -40,7 +40,7 @@ export default { has_details: true, details_path: '/root/ci-mock/builds/4153', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -65,7 +65,7 @@ export default { has_details: true, details_path: '/root/ci-mock/builds/4153', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -85,7 +85,7 @@ export default { has_details: true, details_path: '/root/ci-mock/pipelines/123#test', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', }, path: '/root/ci-mock/pipelines/123#test', dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=test', @@ -105,7 +105,7 @@ export default { has_details: true, details_path: '/root/ci-mock/builds/4166', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -130,7 +130,7 @@ export default { has_details: true, details_path: '/root/ci-mock/builds/4166', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -152,7 +152,7 @@ export default { has_details: true, details_path: '/root/ci-mock/builds/4159', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -177,7 +177,7 @@ export default { has_details: true, details_path: '/root/ci-mock/builds/4159', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -197,7 +197,7 @@ export default { has_details: true, details_path: '/root/ci-mock/pipelines/123#deploy', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', }, path: '/root/ci-mock/pipelines/123#deploy', dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=deploy', diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb index 22b9c631ed8..fdc5c0180e4 100644 --- a/spec/lib/gitlab/favicon_spec.rb +++ b/spec/lib/gitlab/favicon_spec.rb @@ -2,19 +2,19 @@ require 'rails_helper' RSpec.describe Gitlab::Favicon, :request_store do describe '.main' do - it 'defaults to favicon.ico' do + it 'defaults to favicon.png' do allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) - expect(described_class.main).to eq 'favicon.ico' + expect(described_class.main).to match_asset_path '/assets/favicon.png' end it 'has blue favicon for development' do allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) - expect(described_class.main).to eq 'favicon-blue.ico' + expect(described_class.main).to match_asset_path '/assets/favicon-blue.png' end it 'has yellow favicon for canary' do stub_env('CANARY', 'true') - expect(described_class.main).to eq 'favicon-yellow.ico' + expect(described_class.main).to match_asset_path 'favicon-yellow.png' end it 'uses the custom favicon if a favicon appearance is present' do @@ -27,7 +27,7 @@ RSpec.describe Gitlab::Favicon, :request_store do subject { described_class.status_overlay('favicon_status_created') } it 'returns the overlay for the status' do - expect(subject).to eq '/assets/ci_favicons/overlays/favicon_status_created.png' + expect(subject).to match_asset_path '/assets/ci_favicons/favicon_status_created.png' end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 54ef0be67ff..3a6a6c116c2 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -173,7 +173,7 @@ describe JiraService do object: { url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}", title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.", - icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, + icon: { title: "GitLab", url16x16: "http://localhost/favicon.png" }, status: { resolved: true } } ) diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb index 98cd15e248b..52459cd369d 100644 --- a/spec/serializers/build_serializer_spec.rb +++ b/spec/serializers/build_serializer_spec.rb @@ -39,7 +39,7 @@ describe BuildSerializer do expect(subject[:label]).to eq('failed') expect(subject[:tooltip]).to eq('failed
(unknown failure)') expect(subject[:icon]).to eq(status.icon) - expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") + expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png") end end @@ -54,7 +54,7 @@ describe BuildSerializer do expect(subject[:label]).to eq('passed') expect(subject[:tooltip]).to eq('passed') expect(subject[:icon]).to eq(status.icon) - expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") + expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png") end end end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index b741308e2c5..33c8213f9a7 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -174,7 +174,7 @@ describe PipelineSerializer do expect(subject[:text]).to eq(status.text) expect(subject[:label]).to eq(status.label) expect(subject[:icon]).to eq(status.icon) - expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") + expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png") end end end diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb index 559475e571c..73f81405d54 100644 --- a/spec/serializers/status_entity_spec.rb +++ b/spec/serializers/status_entity_spec.rb @@ -18,17 +18,17 @@ describe StatusEntity do it 'contains status details' do expect(subject).to include :text, :icon, :favicon, :label, :group, :tooltip expect(subject).to include :has_details, :details_path - expect(subject[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.ico') + expect(subject[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.png') end it 'contains a dev namespaced favicon if dev env' do allow(Rails.env).to receive(:development?) { true } - expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/dev/favicon_status_success.ico') + expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.png') end it 'contains a canary namespaced favicon if canary env' do stub_env('CANARY', 'true') - expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/canary/favicon_status_success.ico') + expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/canary/favicon_status_success.png') end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index e28b0ea5cf2..b7f38874c26 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -789,7 +789,7 @@ describe SystemNoteService do object: { url: project_commit_url(project, commit), title: "GitLab: Mentioned on commit - #{commit.title}", - icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, + icon: { title: "GitLab", url16x16: "http://localhost/favicon.png" }, status: { resolved: false } } ) @@ -815,7 +815,7 @@ describe SystemNoteService do object: { url: project_issue_url(project, issue), title: "GitLab: Mentioned on issue - #{issue.title}", - icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, + icon: { title: "GitLab", url16x16: "http://localhost/favicon.png" }, status: { resolved: false } } ) @@ -841,7 +841,7 @@ describe SystemNoteService do object: { url: project_snippet_url(project, snippet), title: "GitLab: Mentioned on snippet - #{snippet.title}", - icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, + icon: { title: "GitLab", url16x16: "http://localhost/favicon.png" }, status: { resolved: false } } ) From 9ae08342eb568af47dd815f585ce714aff22ffec Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 7 Dec 2017 13:17:05 +0100 Subject: [PATCH 19/35] force minimagick to use graphicsmagick --- config/initializers/mini_magick.rb | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 config/initializers/mini_magick.rb diff --git a/config/initializers/mini_magick.rb b/config/initializers/mini_magick.rb new file mode 100644 index 00000000000..db0e7bbaaa3 --- /dev/null +++ b/config/initializers/mini_magick.rb @@ -0,0 +1,3 @@ +MiniMagick.configure do |config| + config.cli = :graphicsmagick +end From 36d000cb7884a0ab53f504e91fd0336cd848b32c Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 5 Feb 2018 15:32:28 +0100 Subject: [PATCH 20/35] use inverted style for remove buttons --- app/views/admin/appearances/_form.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index f77e22bcc45..308a779cb88 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -11,7 +11,7 @@ = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview' - if @appearance.persisted? %br - = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo" + = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" %hr = f.hidden_field :header_logo_cache = f.file_field :header_logo, class: "" @@ -38,7 +38,7 @@ = image_tag @appearance.logo_url, class: 'appearance-logo-preview' - if @appearance.persisted? %br - = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo" + = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" %hr = f.hidden_field :logo_cache = f.file_field :logo, class: "" @@ -70,7 +70,7 @@ .js-favicons{ data: { favicon: @appearance.favicon.favicon_main.url, status_overlays: Gitlab::Favicon.available_status_overlays } } - if @appearance.persisted? %br - = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo" + = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" %hr = f.hidden_field :favicon_cache = f.file_field :favicon, class: '' From b4d84c07bcf143aeab7abccb8d0cdb849f605af5 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 5 Feb 2018 15:41:37 +0100 Subject: [PATCH 21/35] remove favicon preview on appearance page --- app/assets/javascripts/favicon_admin.js | 19 ------------------- app/views/admin/appearances/_form.html.haml | 7 +------ lib/gitlab/favicon.rb | 6 ------ spec/features/admin/admin_appearance_spec.rb | 5 ++--- 4 files changed, 3 insertions(+), 34 deletions(-) delete mode 100644 app/assets/javascripts/favicon_admin.js diff --git a/app/assets/javascripts/favicon_admin.js b/app/assets/javascripts/favicon_admin.js deleted file mode 100644 index 97e87054ce0..00000000000 --- a/app/assets/javascripts/favicon_admin.js +++ /dev/null @@ -1,19 +0,0 @@ -import {createOverlayIcon} from '~/lib/utils/common_utils'; - -export default class FaviconAdmin { - constructor() { - const faviconContainer = $('.js-favicons'); - const faviconUrl = faviconContainer.data('favicon'); - const overlayUrls = faviconContainer.data('status-overlays') || []; - - overlayUrls.forEach((statusOverlay) => { - createOverlayIcon(faviconUrl, statusOverlay).then((faviconWithOverlayUrl) => { - const image = $(''); - image.addClass('appearance-light-logo-preview'); - image.attr('src', faviconWithOverlayUrl); - - faviconContainer.append(image); - }); - }); - } -} diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 308a779cb88..81979f7b331 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -62,12 +62,7 @@ = f.label :favicon, 'Favicon', class: 'control-label' .col-sm-10 - if @appearance.favicon? - = image_tag @appearance.favicon.favicon_main.url, class: 'appearance-light-logo-preview js-main-favicon' - - if @appearance.favicon? - = f.label :favicon, 'Status icons preview', class: 'control-label' - .col-sm-10 - - if @appearance.favicon? - .js-favicons{ data: { favicon: @appearance.favicon.favicon_main.url, status_overlays: Gitlab::Favicon.available_status_overlays } } + = image_tag @appearance.favicon.favicon_main.url, class: 'appearance-light-logo-preview' - if @appearance.persisted? %br = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb index d0178d0fdf9..e3e4a18e241 100644 --- a/lib/gitlab/favicon.rb +++ b/lib/gitlab/favicon.rb @@ -18,12 +18,6 @@ module Gitlab ActionController::Base.helpers.image_path(path) end - def available_status_overlays - available_status_names.map do |status_name| - status_overlay(status_name) - end - end - def available_status_names @available_status_names ||= begin Dir.glob(Rails.root.join('app', 'assets', 'images', 'ci_favicons', '*.png')) diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb index 0ac4f111c52..bd879635d2f 100644 --- a/spec/features/admin/admin_appearance_spec.rb +++ b/spec/features/admin/admin_appearance_spec.rb @@ -76,15 +76,14 @@ feature 'Admin Appearance' do expect(page).not_to have_css(header_logo_selector) end - scenario 'Favicon', :js do + scenario 'Favicon' do sign_in(create(:admin)) visit admin_appearances_path attach_file(:appearance_favicon, logo_fixture) click_button 'Save' - # 11 = 1 original + 10 overlay variations - expect(page).to have_css('.appearance-light-logo-preview', count: 11) + expect(page).to have_css('.appearance-light-logo-preview') click_link 'Remove favicon' From 46328b1242d6508f7ee13c6b6e40d9aad6af07ea Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 9 Apr 2018 16:49:41 +0200 Subject: [PATCH 22/35] the '?' favicon hack doesn't seem to be required probably due to recent changes in `UploadsController`. --- lib/gitlab/favicon.rb | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb index e3e4a18e241..d554d100ad1 100644 --- a/lib/gitlab/favicon.rb +++ b/lib/gitlab/favicon.rb @@ -2,7 +2,7 @@ module Gitlab class Favicon class << self def main - return custom_favicon_url(appearance_favicon.favicon_main.url) if appearance_favicon.exists? + return appearance_favicon.favicon_main.url if appearance_favicon.exists? return ActionController::Base.helpers.image_path('favicon-yellow.png') if Gitlab::Utils.to_boolean(ENV['CANARY']) return ActionController::Base.helpers.image_path('favicon-blue.png') if Rails.env.development? @@ -35,13 +35,6 @@ module Gitlab def appearance_favicon appearance.favicon end - - # Without the '?' at the end of the favicon url the custom favicon (i.e. - # the favicons that are served through `UploadController`) are not shown - # in the browser. - def custom_favicon_url(url) - "#{url}?" - end end end end From 256d959729f14094a490c102508e2878c1dd87fc Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 12 Apr 2018 14:11:21 +0200 Subject: [PATCH 23/35] ability to get an image's alternative version --- app/controllers/concerns/uploads_actions.rb | 8 +++++++- spec/controllers/uploads_controller_spec.rb | 22 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index 80049044124..a62d45db43d 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -31,7 +31,13 @@ module UploadsActions disposition = uploader.image_or_video? ? 'inline' : 'attachment' - send_upload(uploader, attachment: uploader.filename, disposition: disposition) + uploader_version = uploader.versions.values.find { |version| version.filename == params[:filename] } + + if uploader_version + return send_upload(uploader_version, attachment: uploader_version.filename, disposition: disposition) + end + + return send_upload(uploader, attachment: uploader.filename, disposition: disposition) end private diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 376b229ffc9..ae62039fb32 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -560,5 +560,27 @@ describe UploadsController do end end end + + context 'the version filename must match' do + let!(:appearance) { create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') } + + context 'has a valid filename on the version file' do + it 'successfully returns the file' do + get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'favicon_main_dk.png' + + expect(response).to have_gitlab_http_status(200) + expect(response.header['Content-Disposition']).to eq 'inline; filename="favicon_main_dk.png"' + end + end + + context 'has an invalid filename on the version file' do + it 'returns the original file' do + get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'favicon_bogusversion_dk.png' + + expect(response).to have_gitlab_http_status(200) + expect(response.header['Content-Disposition']).to eq 'inline; filename="dk.png"' + end + end + end end end From 96d0b1c67bc1f2a2881298ff898954ba00cd563f Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 12 Apr 2018 14:13:06 +0200 Subject: [PATCH 24/35] require uploaded file's name to match in any case --- app/controllers/concerns/uploads_actions.rb | 6 +- spec/controllers/uploads_controller_spec.rb | 92 ++++++++++++--------- 2 files changed, 59 insertions(+), 39 deletions(-) diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index a62d45db43d..98a55a6d82c 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -31,13 +31,17 @@ module UploadsActions disposition = uploader.image_or_video? ? 'inline' : 'attachment' + if uploader.filename == params[:filename] + return send_upload(uploader, attachment: uploader.filename, disposition: disposition) + end + uploader_version = uploader.versions.values.find { |version| version.filename == params[:filename] } if uploader_version return send_upload(uploader_version, attachment: uploader_version.filename, disposition: disposition) end - return send_upload(uploader, attachment: uploader.filename, disposition: disposition) + render_404 end private diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index ae62039fb32..912aa82526a 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -136,7 +136,7 @@ describe UploadsController do context 'for PNG files' do it 'returns Content-Disposition: inline' do note = create(:note, :with_attachment, project: project) - get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' expect(response['Content-Disposition']).to start_with('inline;') end @@ -145,7 +145,7 @@ describe UploadsController do context 'for SVG files' do it 'returns Content-Disposition: attachment' do note = create(:note, :with_svg_attachment, project: project) - get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.svg' + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'unsanitized.svg' expect(response['Content-Disposition']).to start_with('attachment;') end @@ -164,7 +164,7 @@ describe UploadsController do end it "redirects to the sign in page" do - get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png" expect(response).to redirect_to(new_user_session_path) end @@ -172,14 +172,14 @@ describe UploadsController do context "when the user isn't blocked" do it "responds with status 200" do - get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png' + get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png' response end @@ -189,14 +189,14 @@ describe UploadsController do context "when not signed in" do it "responds with status 200" do - get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png' + get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png' response end @@ -214,14 +214,14 @@ describe UploadsController do context "when not signed in" do it "responds with status 200" do - get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' response end @@ -234,14 +234,14 @@ describe UploadsController do end it "responds with status 200" do - get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' response end @@ -256,7 +256,7 @@ describe UploadsController do context "when not signed in" do it "redirects to the sign in page" do - get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" expect(response).to redirect_to(new_user_session_path) end @@ -279,7 +279,7 @@ describe UploadsController do end it "redirects to the sign in page" do - get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" expect(response).to redirect_to(new_user_session_path) end @@ -287,14 +287,14 @@ describe UploadsController do context "when the user isn't blocked" do it "responds with status 200" do - get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' response end @@ -304,7 +304,7 @@ describe UploadsController do context "when the user doesn't have access to the project" do it "responds with status 404" do - get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" expect(response).to have_gitlab_http_status(404) end @@ -319,14 +319,14 @@ describe UploadsController do context "when the group is public" do context "when not signed in" do it "responds with status 200" do - get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' response end @@ -339,14 +339,14 @@ describe UploadsController do end it "responds with status 200" do - get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' response end @@ -375,7 +375,7 @@ describe UploadsController do end it "redirects to the sign in page" do - get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" expect(response).to redirect_to(new_user_session_path) end @@ -383,14 +383,14 @@ describe UploadsController do context "when the user isn't blocked" do it "responds with status 200" do - get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' response end @@ -400,7 +400,7 @@ describe UploadsController do context "when the user doesn't have access to the project" do it "responds with status 404" do - get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" expect(response).to have_gitlab_http_status(404) end @@ -420,14 +420,14 @@ describe UploadsController do context "when not signed in" do it "responds with status 200" do - get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' response end @@ -440,14 +440,14 @@ describe UploadsController do end it "responds with status 200" do - get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' response end @@ -462,7 +462,7 @@ describe UploadsController do context "when not signed in" do it "redirects to the sign in page" do - get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" expect(response).to redirect_to(new_user_session_path) end @@ -485,7 +485,7 @@ describe UploadsController do end it "redirects to the sign in page" do - get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" expect(response).to redirect_to(new_user_session_path) end @@ -493,14 +493,14 @@ describe UploadsController do context "when the user isn't blocked" do it "responds with status 200" do - get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' response end @@ -510,7 +510,7 @@ describe UploadsController do context "when the user doesn't have access to the project" do it "responds with status 404" do - get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" expect(response).to have_gitlab_http_status(404) end @@ -561,24 +561,40 @@ describe UploadsController do end end - context 'the version filename must match' do + context 'original filename or a version filename must match' do let!(:appearance) { create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') } + context 'has a valid filename on the original file' do + it 'successfully returns the file' do + get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'dk.png' + + expect(response).to have_gitlab_http_status(200) + expect(response.header['Content-Disposition']).to end_with 'filename="dk.png"' + end + end + + context 'has an invalid filename on the original file' do + it 'returns a 404' do + get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'bogus.png' + + expect(response).to have_gitlab_http_status(404) + end + end + context 'has a valid filename on the version file' do it 'successfully returns the file' do get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'favicon_main_dk.png' expect(response).to have_gitlab_http_status(200) - expect(response.header['Content-Disposition']).to eq 'inline; filename="favicon_main_dk.png"' + expect(response.header['Content-Disposition']).to end_with 'filename="favicon_main_dk.png"' end end context 'has an invalid filename on the version file' do - it 'returns the original file' do + it 'returns a 404' do get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'favicon_bogusversion_dk.png' - expect(response).to have_gitlab_http_status(200) - expect(response.header['Content-Disposition']).to eq 'inline; filename="dk.png"' + expect(response).to have_gitlab_http_status(404) end end end From b606636eab1d9beb0331703dfca7399bba111f46 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 13 Apr 2018 21:29:01 +0200 Subject: [PATCH 25/35] simplify uploader versions check --- app/controllers/concerns/uploads_actions.rb | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index 98a55a6d82c..170bca8b56f 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -31,17 +31,12 @@ module UploadsActions disposition = uploader.image_or_video? ? 'inline' : 'attachment' - if uploader.filename == params[:filename] - return send_upload(uploader, attachment: uploader.filename, disposition: disposition) - end + uploaders = [uploader, *uploader.versions.values] + uploader = uploaders.find { |version| version.filename == params[:filename] } - uploader_version = uploader.versions.values.find { |version| version.filename == params[:filename] } + return render_404 unless uploader - if uploader_version - return send_upload(uploader_version, attachment: uploader_version.filename, disposition: disposition) - end - - render_404 + send_upload(uploader, attachment: uploader.filename, disposition: disposition) end private From 9151aed773fb32363abbae2fb7a06610915cf882 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 13 Apr 2018 21:40:32 +0200 Subject: [PATCH 26/35] dry up asset path helper calls --- lib/gitlab/favicon.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb index d554d100ad1..451c9daf761 100644 --- a/lib/gitlab/favicon.rb +++ b/lib/gitlab/favicon.rb @@ -3,10 +3,17 @@ module Gitlab class << self def main return appearance_favicon.favicon_main.url if appearance_favicon.exists? - return ActionController::Base.helpers.image_path('favicon-yellow.png') if Gitlab::Utils.to_boolean(ENV['CANARY']) - return ActionController::Base.helpers.image_path('favicon-blue.png') if Rails.env.development? - ActionController::Base.helpers.image_path('favicon.png') + image_name = + if Gitlab::Utils.to_boolean(ENV['CANARY']) + 'favicon-yellow.png' + elsif Rails.env.development? + 'favicon-blue.png' + else + 'favicon.png' + end + + ActionController::Base.helpers.image_path(image_name) end def status_overlay(status_name) From 7706b3a471de37240c0da6fcc14c7d38c6d62eb9 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 13 Apr 2018 22:30:08 +0200 Subject: [PATCH 27/35] use Gitlab::Favicon for jira service --- app/models/project_services/jira_service.rb | 2 +- .../project_services/jira_service_spec.rb | 18 +++++++++++++++++- spec/services/system_note_service_spec.rb | 8 +++++--- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 2edfe7d81f8..412d62388f0 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -265,7 +265,7 @@ class JiraService < IssueTrackerService title: title, status: status, icon: { - title: 'GitLab', url16x16: asset_url('favicon.png', host: gitlab_config.url) + title: 'GitLab', url16x16: asset_url(Gitlab::Favicon.main, host: gitlab_config.url) } } } diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 3a6a6c116c2..50bdb80ff92 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -164,6 +164,8 @@ describe JiraService do it "creates Remote Link reference in JIRA for comment" do @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) + favicon_path = "http://localhost/assets/#{Rails.application.assets.find_asset('favicon.png').digest_path}" + # Creates comment expect(WebMock).to have_requested(:post, @comment_url) # Creates Remote Link in JIRA issue fields @@ -173,7 +175,7 @@ describe JiraService do object: { url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}", title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.", - icon: { title: "GitLab", url16x16: "http://localhost/favicon.png" }, + icon: { title: "GitLab", url16x16: favicon_path }, status: { resolved: true } } ) @@ -464,4 +466,18 @@ describe JiraService do end end end + + describe 'favicon urls', :request_store do + it 'includes the standard favicon' do + props = described_class.new.send(:build_remote_link_props, url: 'http://example.com', title: 'title') + expect(props[:object][:icon][:url16x16]).to match %r{^http://localhost/assets/favicon(?:-\h+).png$} + end + + it 'includes returns the custom favicon' do + create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) + + props = described_class.new.send(:build_remote_link_props, url: 'http://example.com', title: 'title') + expect(props[:object][:icon][:url16x16]).to match %r{^http://localhost/uploads/-/system/appearance/favicon/\d+/favicon_main_dk.png$} + end + end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index b7f38874c26..e2ee421921c 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -769,6 +769,8 @@ describe SystemNoteService do end describe "new reference" do + let(:favicon_path) { "http://localhost/assets/#{Rails.application.assets.find_asset('favicon.png').digest_path}" } + before do allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) end @@ -789,7 +791,7 @@ describe SystemNoteService do object: { url: project_commit_url(project, commit), title: "GitLab: Mentioned on commit - #{commit.title}", - icon: { title: "GitLab", url16x16: "http://localhost/favicon.png" }, + icon: { title: "GitLab", url16x16: favicon_path }, status: { resolved: false } } ) @@ -815,7 +817,7 @@ describe SystemNoteService do object: { url: project_issue_url(project, issue), title: "GitLab: Mentioned on issue - #{issue.title}", - icon: { title: "GitLab", url16x16: "http://localhost/favicon.png" }, + icon: { title: "GitLab", url16x16: favicon_path }, status: { resolved: false } } ) @@ -841,7 +843,7 @@ describe SystemNoteService do object: { url: project_snippet_url(project, snippet), title: "GitLab: Mentioned on snippet - #{snippet.title}", - icon: { title: "GitLab", url16x16: "http://localhost/favicon.png" }, + icon: { title: "GitLab", url16x16: favicon_path }, status: { resolved: false } } ) From 1e9c33acd1129557124e330df2f178fb097d67e5 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Sat, 14 Apr 2018 15:37:32 +0200 Subject: [PATCH 28/35] remove obsolete favicon related spec the favicon variations don't need to be tested on the entity anymore, as they are A) tested in the dedicated `Gitlab::Favicon` and B) they are created on the client (i.e. the overlays are always the same for all base favicon variants). --- spec/serializers/status_entity_spec.rb | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb index 73f81405d54..0b010ebd507 100644 --- a/spec/serializers/status_entity_spec.rb +++ b/spec/serializers/status_entity_spec.rb @@ -20,15 +20,5 @@ describe StatusEntity do expect(subject).to include :has_details, :details_path expect(subject[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.png') end - - it 'contains a dev namespaced favicon if dev env' do - allow(Rails.env).to receive(:development?) { true } - expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.png') - end - - it 'contains a canary namespaced favicon if canary env' do - stub_env('CANARY', 'true') - expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/canary/favicon_status_success.png') - end end end From 197932a2225904898778e7edadc0e447b91cc2ef Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 30 Apr 2018 12:45:07 +0200 Subject: [PATCH 29/35] allow only png, ico for favicon uploads the related omnibus graphicsmagick package only supports those formats. see https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1975 --- app/uploaders/favicon_uploader.rb | 2 +- spec/features/admin/admin_appearance_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb index d7be77477b2..aa4c78da7a6 100644 --- a/app/uploaders/favicon_uploader.rb +++ b/app/uploaders/favicon_uploader.rb @@ -11,7 +11,7 @@ class FaviconUploader < AttachmentUploader end def extension_whitelist - UploaderHelper::IMAGE_EXT + %w[png ico] end private diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb index bd879635d2f..a5e0ac592b9 100644 --- a/spec/features/admin/admin_appearance_spec.rb +++ b/spec/features/admin/admin_appearance_spec.rb @@ -93,7 +93,7 @@ feature 'Admin Appearance' do attach_file(:appearance_favicon, Rails.root.join('spec', 'fixtures', 'sanitized.svg')) click_button 'Save' - expect(page).to have_content 'Favicon You are not allowed to upload "svg" files, allowed types: png, jpg, jpeg, gif, bmp, tiff, ico' + expect(page).to have_content 'Favicon You are not allowed to upload "svg" files, allowed types: png, ico' end def expect_custom_sign_in_appearance(appearance) From cb564d5831cba9ca8403440a5cbe02d968260da1 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 15 May 2018 14:47:04 +0200 Subject: [PATCH 30/35] use build image with picturemagick --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1679ae378c9..e366538d907 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29" .dedicated-runner: &dedicated-runner retry: 1 From d2256300e559e836884fea57210266dae764f13e Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 17 May 2018 19:00:32 +0200 Subject: [PATCH 31/35] hint the allowed image formats on favicon upload --- app/helpers/favicon_helper.rb | 7 +++++++ app/uploaders/favicon_uploader.rb | 4 +++- app/views/admin/appearances/_form.html.haml | 4 +++- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 app/helpers/favicon_helper.rb diff --git a/app/helpers/favicon_helper.rb b/app/helpers/favicon_helper.rb new file mode 100644 index 00000000000..3a5342a8d9d --- /dev/null +++ b/app/helpers/favicon_helper.rb @@ -0,0 +1,7 @@ +module FaviconHelper + def favicon_extension_whitelist + FaviconUploader::EXTENSION_WHITELIST + .map { |extension| "'.#{extension}'"} + .to_sentence + end +end diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb index aa4c78da7a6..09afc63a5aa 100644 --- a/app/uploaders/favicon_uploader.rb +++ b/app/uploaders/favicon_uploader.rb @@ -1,4 +1,6 @@ class FaviconUploader < AttachmentUploader + EXTENSION_WHITELIST = %w[png ico].freeze + include CarrierWave::MiniMagick version :favicon_main do @@ -11,7 +13,7 @@ class FaviconUploader < AttachmentUploader end def extension_whitelist - %w[png ico] + EXTENSION_WHITELIST end private diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 81979f7b331..ac92b043074 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -70,7 +70,9 @@ = f.hidden_field :favicon_cache = f.file_field :favicon, class: '' .hint - Maximum file size is 1MB. The resulting favicons will be cropped to be square and scaled down to a size of 32x32 px. + Maximum file size is 1MB. Allowed image formats are #{favicon_extension_whitelist}. + %br + The resulting favicons will be cropped to be square and scaled down to a size of 32x32 px. .form-actions = f.submit 'Save', class: 'btn btn-save append-right-10' From 29598f6e6d8ebbe9c1b64018fcb925655ccceb67 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 22 May 2018 18:37:35 +0200 Subject: [PATCH 32/35] find assets in test for CI and local test Due to the change in https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14583/diffs we can't use the same method to access assets in a CI and the local test environment anymore. --- spec/models/project_services/jira_service_spec.rb | 3 ++- spec/services/system_note_service_spec.rb | 3 ++- spec/support/helpers/assets_helpers.rb | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 spec/support/helpers/assets_helpers.rb diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 50bdb80ff92..c3b4eb17a5c 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe JiraService do include Gitlab::Routing + include AssetsHelpers describe '#options' do let(:service) do @@ -164,7 +165,7 @@ describe JiraService do it "creates Remote Link reference in JIRA for comment" do @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) - favicon_path = "http://localhost/assets/#{Rails.application.assets.find_asset('favicon.png').digest_path}" + favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}" # Creates comment expect(WebMock).to have_requested(:post, @comment_url) diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index e2ee421921c..57d081cffb3 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe SystemNoteService do include Gitlab::Routing include RepoHelpers + include AssetsHelpers set(:group) { create(:group) } set(:project) { create(:project, :repository, group: group) } @@ -769,7 +770,7 @@ describe SystemNoteService do end describe "new reference" do - let(:favicon_path) { "http://localhost/assets/#{Rails.application.assets.find_asset('favicon.png').digest_path}" } + let(:favicon_path) { "http://localhost/assets/#{find_asset('favicon.png').digest_path}" } before do allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) diff --git a/spec/support/helpers/assets_helpers.rb b/spec/support/helpers/assets_helpers.rb new file mode 100644 index 00000000000..09bbf451671 --- /dev/null +++ b/spec/support/helpers/assets_helpers.rb @@ -0,0 +1,15 @@ +module AssetsHelpers + # In a CI environment the assets are not compiled, as there is a CI job + # `compile-assets` that compiles them in the prepare stage for all following + # specs. + # Locally the assets are precompiled dynamically. + # + # Sprockets doesn't provide one method to access an asset for both cases. + def find_asset(asset_name) + if ENV['CI'] + Sprockets::Railtie.build_environment(Rails.application, true)[asset_name] + else + Rails.application.assets.find_asset(asset_name) + end + end +end From 6b72c2ff34e43a7bc1269c2459057e8817ef3595 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 5 Jun 2018 17:32:22 +0200 Subject: [PATCH 33/35] move favicon admin section up --- app/views/admin/appearances/_form.html.haml | 38 ++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index ac92b043074..94db374040c 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -18,6 +18,25 @@ .hint Maximum file size is 1MB. Pages are optimized for a 28px tall header logo + %fieldset.app_logo + %legend + Favicon: + .form-group.row + = f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label' + .col-sm-10 + - if @appearance.favicon? + = image_tag @appearance.favicon.favicon_main.url, class: 'appearance-light-logo-preview' + - if @appearance.persisted? + %br + = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" + %hr + = f.hidden_field :favicon_cache + = f.file_field :favicon, class: '' + .hint + Maximum file size is 1MB. Allowed image formats are #{favicon_extension_whitelist}. + %br + The resulting favicons will be cropped to be square and scaled down to a size of 32x32 px. + %fieldset.sign-in %legend Sign in/Sign up pages: @@ -55,25 +74,6 @@ .hint Guidelines parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}. - %fieldset.app_logo - %legend - Favicon: - .form-group - = f.label :favicon, 'Favicon', class: 'control-label' - .col-sm-10 - - if @appearance.favicon? - = image_tag @appearance.favicon.favicon_main.url, class: 'appearance-light-logo-preview' - - if @appearance.persisted? - %br - = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" - %hr - = f.hidden_field :favicon_cache - = f.file_field :favicon, class: '' - .hint - Maximum file size is 1MB. Allowed image formats are #{favicon_extension_whitelist}. - %br - The resulting favicons will be cropped to be square and scaled down to a size of 32x32 px. - .form-actions = f.submit 'Save', class: 'btn btn-save append-right-10' - if @appearance.persisted? From 5e78ac2a4f3b907679114193d971dcdf7b29dcc2 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 6 Jun 2018 10:16:13 +0200 Subject: [PATCH 34/35] document custom favicon --- doc/administration/index.md | 1 + doc/customization/favicon.md | 16 ++++++++++++++++ doc/customization/favicon/appearance.png | Bin 0 -> 52245 bytes doc/customization/favicon/custom_favicon.png | Bin 0 -> 60083 bytes 4 files changed, 17 insertions(+) create mode 100644 doc/customization/favicon.md create mode 100644 doc/customization/favicon/appearance.png create mode 100644 doc/customization/favicon/custom_favicon.png diff --git a/doc/administration/index.md b/doc/administration/index.md index df935095e61..0e65f9a9963 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -49,6 +49,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. #### Customizing GitLab's appearance - [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers. +- [Favicon](../customization/favicon.md): Change the default favicon to your own logo. - [Branded login page](../customization/branded_login_page.md): Customize the login page with your own logo, title, and description. - [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page. - ["New Project" page](../customization/new_project_page.md): Customize the text to be displayed on the page that opens whenever your users create a new project. diff --git a/doc/customization/favicon.md b/doc/customization/favicon.md new file mode 100644 index 00000000000..45a18159b5e --- /dev/null +++ b/doc/customization/favicon.md @@ -0,0 +1,16 @@ +# Changing the favicon + +> [Introduced][ce-14497] in GitLab 11.0. + +[ce-14497]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14497 + +Navigate to the **Admin** area and go to the **Appearance** page. + +Upload the custom favicon (**Favicon**) in the section **Favicon**. + +![appearance](favicon/appearance.png) + +After saving the page, the new favicon will be shown in the browser. The main +favicon as well as the CI status icons will show the custom icon: + +![custom_favicon](favicon/custom_favicon.png) diff --git a/doc/customization/favicon/appearance.png b/doc/customization/favicon/appearance.png new file mode 100644 index 0000000000000000000000000000000000000000..6c41a05fc1f04e055fefb4d1224e3e67b37931e0 GIT binary patch literal 52245 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 (i_S<>A}*L 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 Nl;zds%4ID>{tL3tpz;6! literal 0 HcmV?d00001 From 366e1331692900300df42e9c38fc17bd46b7ca1c Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 6 Jun 2018 10:23:44 +0200 Subject: [PATCH 35/35] add changelog for custom favicon --- changelogs/unreleased/feature-customizable-favicon.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/feature-customizable-favicon.yml diff --git a/changelogs/unreleased/feature-customizable-favicon.yml b/changelogs/unreleased/feature-customizable-favicon.yml new file mode 100644 index 00000000000..0e5afc17c9e --- /dev/null +++ b/changelogs/unreleased/feature-customizable-favicon.yml @@ -0,0 +1,5 @@ +--- +title: Allow changing the default favicon to a custom icon. +merge_request: 14497 +author: Alexis Reigel +type: added