summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/auth_buttons/dingtalk_64.pngbin0 -> 1623 bytes
-rw-r--r--app/assets/images/emoji.pngbin1219696 -> 1356857 bytes
-rw-r--r--app/assets/images/emoji/100.pngbin793 -> 0 bytes
-rw-r--r--app/assets/images/emoji/1234.pngbin676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/8ball.pngbin810 -> 0 bytes
-rw-r--r--app/assets/images/emoji/a.pngbin469 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ab.pngbin505 -> 0 bytes
-rw-r--r--app/assets/images/emoji/abc.pngbin646 -> 0 bytes
-rw-r--r--app/assets/images/emoji/abcd.pngbin670 -> 0 bytes
-rw-r--r--app/assets/images/emoji/accept.pngbin491 -> 0 bytes
-rw-r--r--app/assets/images/emoji/aerial_tramway.pngbin759 -> 0 bytes
-rw-r--r--app/assets/images/emoji/airplane.pngbin1152 -> 0 bytes
-rw-r--r--app/assets/images/emoji/airplane_arriving.pngbin1101 -> 0 bytes
-rw-r--r--app/assets/images/emoji/airplane_departure.pngbin1111 -> 0 bytes
-rw-r--r--app/assets/images/emoji/airplane_small.pngbin1229 -> 0 bytes
-rw-r--r--app/assets/images/emoji/alarm_clock.pngbin1044 -> 0 bytes
-rw-r--r--app/assets/images/emoji/alembic.pngbin953 -> 0 bytes
-rw-r--r--app/assets/images/emoji/alien.pngbin839 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ambulance.pngbin1238 -> 0 bytes
-rw-r--r--app/assets/images/emoji/amphora.pngbin1044 -> 0 bytes
-rw-r--r--app/assets/images/emoji/anchor.pngbin779 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angel.pngbin2077 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angel_tone1.pngbin2088 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angel_tone2.pngbin2075 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angel_tone3.pngbin2078 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angel_tone4.pngbin2076 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angel_tone5.pngbin2078 -> 0 bytes
-rw-r--r--app/assets/images/emoji/anger.pngbin594 -> 0 bytes
-rw-r--r--app/assets/images/emoji/anger_right.pngbin551 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angry.pngbin845 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ant.pngbin1412 -> 0 bytes
-rw-r--r--app/assets/images/emoji/apple.pngbin655 -> 0 bytes
-rw-r--r--app/assets/images/emoji/aquarius.pngbin648 -> 0 bytes
-rw-r--r--app/assets/images/emoji/aries.pngbin711 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_backward.pngbin429 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_double_down.pngbin543 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_double_up.pngbin535 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_down.pngbin512 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_down_small.pngbin455 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_forward.pngbin429 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_heading_down.pngbin563 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_heading_up.pngbin559 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_left.pngbin471 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_lower_left.pngbin520 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_lower_right.pngbin526 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_right.pngbin468 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_right_hook.pngbin644 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_up.pngbin507 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_up_down.pngbin474 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_up_small.pngbin454 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_upper_left.pngbin521 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_upper_right.pngbin524 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrows_clockwise.pngbin519 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrows_counterclockwise.pngbin693 -> 0 bytes
-rw-r--r--app/assets/images/emoji/art.pngbin1455 -> 0 bytes
-rw-r--r--app/assets/images/emoji/articulated_lorry.pngbin1710 -> 0 bytes
-rw-r--r--app/assets/images/emoji/asterisk.pngbin627 -> 0 bytes
-rw-r--r--app/assets/images/emoji/astonished.pngbin862 -> 0 bytes
-rw-r--r--app/assets/images/emoji/athletic_shoe.pngbin1595 -> 0 bytes
-rw-r--r--app/assets/images/emoji/atm.pngbin1397 -> 0 bytes
-rw-r--r--app/assets/images/emoji/atom.pngbin912 -> 0 bytes
-rw-r--r--app/assets/images/emoji/avocado.pngbin1520 -> 0 bytes
-rw-r--r--app/assets/images/emoji/b.pngbin391 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby.pngbin1380 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_bottle.pngbin818 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_chick.pngbin1181 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_symbol.pngbin665 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_tone1.pngbin1392 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_tone2.pngbin1392 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_tone3.pngbin1403 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_tone4.pngbin1413 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_tone5.pngbin1405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/back.pngbin562 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bacon.pngbin2148 -> 0 bytes
-rw-r--r--app/assets/images/emoji/badminton.pngbin1253 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baggage_claim.pngbin490 -> 0 bytes
-rw-r--r--app/assets/images/emoji/balloon.pngbin501 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ballot_box.pngbin1355 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ballot_box_with_check.pngbin639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bamboo.pngbin1946 -> 0 bytes
-rw-r--r--app/assets/images/emoji/banana.pngbin1157 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bangbang.pngbin390 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bank.pngbin1358 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bar_chart.pngbin408 -> 0 bytes
-rw-r--r--app/assets/images/emoji/barber.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baseball.pngbin1185 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball.pngbin1546 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball_player.pngbin1491 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone1.pngbin1492 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone2.pngbin1493 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone3.pngbin1492 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone4.pngbin1491 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone5.pngbin1474 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bat.pngbin1190 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bath.pngbin1238 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bath_tone1.pngbin1235 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bath_tone2.pngbin1231 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bath_tone3.pngbin1236 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bath_tone4.pngbin1252 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bath_tone5.pngbin1239 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bathtub.pngbin767 -> 0 bytes
-rw-r--r--app/assets/images/emoji/battery.pngbin228 -> 0 bytes
-rw-r--r--app/assets/images/emoji/beach.pngbin942 -> 0 bytes
-rw-r--r--app/assets/images/emoji/beach_umbrella.pngbin1486 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bear.pngbin1023 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bed.pngbin1572 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bee.pngbin1378 -> 0 bytes
-rw-r--r--app/assets/images/emoji/beer.pngbin1338 -> 0 bytes
-rw-r--r--app/assets/images/emoji/beers.pngbin2100 -> 0 bytes
-rw-r--r--app/assets/images/emoji/beetle.pngbin1288 -> 0 bytes
-rw-r--r--app/assets/images/emoji/beginner.pngbin545 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bell.pngbin1496 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bellhop.pngbin891 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bento.pngbin1127 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bicyclist.pngbin1911 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone1.pngbin1860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone2.pngbin1866 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone3.pngbin1851 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone4.pngbin1852 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone5.pngbin1840 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bike.pngbin1505 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bikini.pngbin613 -> 0 bytes
-rw-r--r--app/assets/images/emoji/biohazard.pngbin794 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bird.pngbin1068 -> 0 bytes
-rw-r--r--app/assets/images/emoji/birthday.pngbin2219 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_circle.pngbin374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_heart.pngbin435 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_joker.pngbin1091 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_large_square.pngbin110 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_medium_small_square.pngbin110 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_medium_square.pngbin108 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_nib.pngbin620 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_small_square.pngbin108 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_square_button.pngbin122 -> 0 bytes
-rw-r--r--app/assets/images/emoji/blossom.pngbin867 -> 0 bytes
-rw-r--r--app/assets/images/emoji/blowfish.pngbin1620 -> 0 bytes
-rw-r--r--app/assets/images/emoji/blue_book.pngbin1347 -> 0 bytes
-rw-r--r--app/assets/images/emoji/blue_car.pngbin1275 -> 0 bytes
-rw-r--r--app/assets/images/emoji/blue_heart.pngbin435 -> 0 bytes
-rw-r--r--app/assets/images/emoji/blush.pngbin812 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boar.pngbin1366 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bomb.pngbin702 -> 0 bytes
-rw-r--r--app/assets/images/emoji/book.pngbin1716 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bookmark.pngbin747 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bookmark_tabs.pngbin1395 -> 0 bytes
-rw-r--r--app/assets/images/emoji/books.pngbin2474 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boom.pngbin1110 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boot.pngbin662 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bouquet.pngbin1662 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow_and_arrow.pngbin1402 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow_tone1.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow_tone2.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow_tone3.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow_tone4.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow_tone5.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bowling.pngbin1426 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boxing_glove.pngbin1575 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boy.pngbin881 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boy_tone1.pngbin876 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boy_tone2.pngbin876 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boy_tone3.pngbin876 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boy_tone4.pngbin870 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boy_tone5.pngbin873 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bread.pngbin1419 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil.pngbin2452 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone1.pngbin2464 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone2.pngbin2457 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone3.pngbin2463 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone4.pngbin2463 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone5.pngbin2462 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bridge_at_night.pngbin637 -> 0 bytes
-rw-r--r--app/assets/images/emoji/briefcase.pngbin1275 -> 0 bytes
-rw-r--r--app/assets/images/emoji/broken_heart.pngbin556 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bug.pngbin1599 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bulb.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bullettrain_front.pngbin1450 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bullettrain_side.pngbin1538 -> 0 bytes
-rw-r--r--app/assets/images/emoji/burrito.pngbin2938 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bus.pngbin1086 -> 0 bytes
-rw-r--r--app/assets/images/emoji/busstop.pngbin626 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bust_in_silhouette.pngbin426 -> 0 bytes
-rw-r--r--app/assets/images/emoji/busts_in_silhouette.pngbin526 -> 0 bytes
-rw-r--r--app/assets/images/emoji/butterfly.pngbin1981 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cactus.pngbin628 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cake.pngbin2266 -> 0 bytes
-rw-r--r--app/assets/images/emoji/calendar.pngbin2077 -> 0 bytes
-rw-r--r--app/assets/images/emoji/calendar_spiral.pngbin1491 -> 0 bytes
-rw-r--r--app/assets/images/emoji/call_me.pngbin894 -> 0 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone1.pngbin893 -> 0 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone2.pngbin891 -> 0 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone3.pngbin891 -> 0 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone4.pngbin891 -> 0 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone5.pngbin893 -> 0 bytes
-rw-r--r--app/assets/images/emoji/calling.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/camel.pngbin1190 -> 0 bytes
-rw-r--r--app/assets/images/emoji/camera.pngbin1783 -> 0 bytes
-rw-r--r--app/assets/images/emoji/camera_with_flash.pngbin2097 -> 0 bytes
-rw-r--r--app/assets/images/emoji/camping.pngbin1513 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cancer.pngbin729 -> 0 bytes
-rw-r--r--app/assets/images/emoji/candle.pngbin1250 -> 0 bytes
-rw-r--r--app/assets/images/emoji/candy.pngbin1054 -> 0 bytes
-rw-r--r--app/assets/images/emoji/canoe.pngbin1244 -> 0 bytes
-rw-r--r--app/assets/images/emoji/capital_abcd.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/capricorn.pngbin688 -> 0 bytes
-rw-r--r--app/assets/images/emoji/card_box.pngbin1523 -> 0 bytes
-rw-r--r--app/assets/images/emoji/card_index.pngbin1929 -> 0 bytes
-rw-r--r--app/assets/images/emoji/carousel_horse.pngbin1739 -> 0 bytes
-rw-r--r--app/assets/images/emoji/carrot.pngbin1236 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cartwheel.pngbin1233 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone1.pngbin1234 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone2.pngbin1235 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone3.pngbin1229 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone4.pngbin1227 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone5.pngbin1214 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cat.pngbin1354 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cat2.pngbin1781 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cd.pngbin908 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chains.pngbin708 -> 0 bytes
-rw-r--r--app/assets/images/emoji/champagne.pngbin1205 -> 0 bytes
-rw-r--r--app/assets/images/emoji/champagne_glass.pngbin1984 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chart.pngbin724 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chart_with_downwards_trend.pngbin709 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chart_with_upwards_trend.pngbin688 -> 0 bytes
-rw-r--r--app/assets/images/emoji/checkered_flag.pngbin787 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cheese.pngbin1697 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cherries.pngbin1211 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cherry_blossom.pngbin1129 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chestnut.pngbin1337 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chicken.pngbin1267 -> 0 bytes
-rw-r--r--app/assets/images/emoji/children_crossing.pngbin778 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chipmunk.pngbin1454 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chocolate_bar.pngbin771 -> 0 bytes
-rw-r--r--app/assets/images/emoji/christmas_tree.pngbin1542 -> 0 bytes
-rw-r--r--app/assets/images/emoji/church.pngbin1298 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cinema.pngbin585 -> 0 bytes
-rw-r--r--app/assets/images/emoji/circus_tent.pngbin1369 -> 0 bytes
-rw-r--r--app/assets/images/emoji/city_dusk.pngbin431 -> 0 bytes
-rw-r--r--app/assets/images/emoji/city_sunset.pngbin997 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cityscape.pngbin599 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cl.pngbin393 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clap.pngbin1456 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clap_tone1.pngbin1458 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clap_tone2.pngbin1458 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clap_tone3.pngbin1458 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clap_tone4.pngbin1458 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clap_tone5.pngbin1444 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clapper.pngbin1535 -> 0 bytes
-rw-r--r--app/assets/images/emoji/classical_building.pngbin1006 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clipboard.pngbin1345 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock.pngbin592 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock1.pngbin586 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock10.pngbin593 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock1030.pngbin530 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock11.pngbin590 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock1130.pngbin583 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock12.pngbin480 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock1230.pngbin579 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock130.pngbin526 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock2.pngbin591 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock230.pngbin576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock3.pngbin482 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock330.pngbin568 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock4.pngbin592 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock430.pngbin531 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock5.pngbin585 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock530.pngbin552 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock6.pngbin466 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock630.pngbin536 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock7.pngbin581 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock730.pngbin531 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock8.pngbin590 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock830.pngbin570 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock9.pngbin484 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock930.pngbin576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/closed_book.pngbin1359 -> 0 bytes
-rw-r--r--app/assets/images/emoji/closed_lock_with_key.pngbin1250 -> 0 bytes
-rw-r--r--app/assets/images/emoji/closed_umbrella.pngbin1002 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cloud.pngbin626 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cloud_lightning.pngbin767 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cloud_rain.pngbin876 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cloud_snow.pngbin823 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cloud_tornado.pngbin1519 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clown.pngbin1818 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clubs.pngbin458 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cocktail.pngbin1027 -> 0 bytes
-rw-r--r--app/assets/images/emoji/coffee.pngbin1679 -> 0 bytes
-rw-r--r--app/assets/images/emoji/coffin.pngbin2195 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cold_sweat.pngbin971 -> 0 bytes
-rw-r--r--app/assets/images/emoji/comet.pngbin1819 -> 0 bytes
-rw-r--r--app/assets/images/emoji/compression.pngbin1612 -> 0 bytes
-rw-r--r--app/assets/images/emoji/computer.pngbin369 -> 0 bytes
-rw-r--r--app/assets/images/emoji/confetti_ball.pngbin1703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/confounded.pngbin844 -> 0 bytes
-rw-r--r--app/assets/images/emoji/confused.pngbin647 -> 0 bytes
-rw-r--r--app/assets/images/emoji/congratulations.pngbin729 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_site.pngbin668 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_worker.pngbin1126 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone1.pngbin1102 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone2.pngbin1102 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone3.pngbin1102 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone4.pngbin1095 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone5.pngbin1119 -> 0 bytes
-rw-r--r--app/assets/images/emoji/control_knobs.pngbin1104 -> 0 bytes
-rw-r--r--app/assets/images/emoji/convenience_store.pngbin528 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cookie.pngbin1351 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cooking.pngbin764 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cool.pngbin396 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cop.pngbin1440 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cop_tone1.pngbin1421 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cop_tone2.pngbin1424 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cop_tone3.pngbin1419 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cop_tone4.pngbin1417 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cop_tone5.pngbin1433 -> 0 bytes
-rw-r--r--app/assets/images/emoji/copyright.pngbin530 -> 0 bytes
-rw-r--r--app/assets/images/emoji/corn.pngbin1547 -> 0 bytes
-rw-r--r--app/assets/images/emoji/couch.pngbin1362 -> 0 bytes
-rw-r--r--app/assets/images/emoji/couple.pngbin1537 -> 0 bytes
-rw-r--r--app/assets/images/emoji/couple_mm.pngbin1091 -> 0 bytes
-rw-r--r--app/assets/images/emoji/couple_with_heart.pngbin1285 -> 0 bytes
-rw-r--r--app/assets/images/emoji/couple_ww.pngbin1034 -> 0 bytes
-rw-r--r--app/assets/images/emoji/couplekiss.pngbin1380 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cow.pngbin1640 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cow2.pngbin1810 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cowboy.pngbin1353 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crab.pngbin1475 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crayon.pngbin633 -> 0 bytes
-rw-r--r--app/assets/images/emoji/credit_card.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crescent_moon.pngbin446 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cricket.pngbin1060 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crocodile.pngbin2408 -> 0 bytes
-rw-r--r--app/assets/images/emoji/croissant.pngbin1313 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cross.pngbin408 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crossed_flags.pngbin1239 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crossed_swords.pngbin1591 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crown.pngbin1534 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cruise_ship.pngbin2272 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cry.pngbin1123 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crying_cat_face.pngbin1875 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crystal_ball.pngbin1913 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cucumber.pngbin1357 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cupid.pngbin846 -> 0 bytes
-rw-r--r--app/assets/images/emoji/curly_loop.pngbin545 -> 0 bytes
-rw-r--r--app/assets/images/emoji/currency_exchange.pngbin576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/curry.pngbin1754 -> 0 bytes
-rw-r--r--app/assets/images/emoji/custard.pngbin1273 -> 0 bytes
-rw-r--r--app/assets/images/emoji/customs.pngbin648 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cyclone.pngbin797 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dagger.pngbin916 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancer.pngbin1405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone1.pngbin1420 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone2.pngbin1423 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone3.pngbin1429 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone4.pngbin1428 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone5.pngbin1418 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancers.pngbin1872 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dango.pngbin802 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dark_sunglasses.pngbin829 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dart.pngbin1374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dash.pngbin840 -> 0 bytes
-rw-r--r--app/assets/images/emoji/date.pngbin788 -> 0 bytes
-rw-r--r--app/assets/images/emoji/deciduous_tree.pngbin1267 -> 0 bytes
-rw-r--r--app/assets/images/emoji/deer.pngbin1606 -> 0 bytes
-rw-r--r--app/assets/images/emoji/department_store.pngbin673 -> 0 bytes
-rw-r--r--app/assets/images/emoji/desert.pngbin1443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/desktop.pngbin311 -> 0 bytes
-rw-r--r--app/assets/images/emoji/diamond_shape_with_a_dot_inside.pngbin693 -> 0 bytes
-rw-r--r--app/assets/images/emoji/diamonds.pngbin247 -> 0 bytes
-rw-r--r--app/assets/images/emoji/disappointed.pngbin757 -> 0 bytes
-rw-r--r--app/assets/images/emoji/disappointed_relieved.pngbin835 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dividers.pngbin810 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dizzy.pngbin795 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dizzy_face.pngbin710 -> 0 bytes
-rw-r--r--app/assets/images/emoji/do_not_litter.pngbin1010 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dog.pngbin1674 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dog2.pngbin2085 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dollar.pngbin405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dolls.pngbin2249 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dolphin.pngbin1697 -> 0 bytes
-rw-r--r--app/assets/images/emoji/door.pngbin1105 -> 0 bytes
-rw-r--r--app/assets/images/emoji/doughnut.pngbin1322 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dove.pngbin967 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dragon.pngbin1574 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dragon_face.pngbin1769 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dress.pngbin1001 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dromedary_camel.pngbin1515 -> 0 bytes
-rw-r--r--app/assets/images/emoji/drooling_face.pngbin1049 -> 0 bytes
-rw-r--r--app/assets/images/emoji/droplet.pngbin411 -> 0 bytes
-rw-r--r--app/assets/images/emoji/drum.pngbin1870 -> 0 bytes
-rw-r--r--app/assets/images/emoji/duck.pngbin1729 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dvd.pngbin933 -> 0 bytes
-rw-r--r--app/assets/images/emoji/e-mail.pngbin1196 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eagle.pngbin2222 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear.pngbin860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear_of_rice.pngbin1422 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear_tone1.pngbin860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear_tone2.pngbin860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear_tone3.pngbin860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear_tone4.pngbin860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear_tone5.pngbin860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/earth_africa.pngbin978 -> 0 bytes
-rw-r--r--app/assets/images/emoji/earth_americas.pngbin1031 -> 0 bytes
-rw-r--r--app/assets/images/emoji/earth_asia.pngbin966 -> 0 bytes
-rw-r--r--app/assets/images/emoji/egg.pngbin710 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eggplant.pngbin773 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eight.pngbin608 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eight_pointed_black_star.pngbin493 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eight_spoked_asterisk.pngbin493 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eject.pngbin548 -> 0 bytes
-rw-r--r--app/assets/images/emoji/electric_plug.pngbin548 -> 0 bytes
-rw-r--r--app/assets/images/emoji/elephant.pngbin1293 -> 0 bytes
-rw-r--r--app/assets/images/emoji/end.pngbin393 -> 0 bytes
-rw-r--r--app/assets/images/emoji/envelope.pngbin916 -> 0 bytes
-rw-r--r--app/assets/images/emoji/envelope_with_arrow.pngbin1062 -> 0 bytes
-rw-r--r--app/assets/images/emoji/euro.pngbin460 -> 0 bytes
-rw-r--r--app/assets/images/emoji/european_castle.pngbin965 -> 0 bytes
-rw-r--r--app/assets/images/emoji/european_post_office.pngbin551 -> 0 bytes
-rw-r--r--app/assets/images/emoji/evergreen_tree.pngbin719 -> 0 bytes
-rw-r--r--app/assets/images/emoji/exclamation.pngbin354 -> 0 bytes
-rw-r--r--app/assets/images/emoji/expressionless.pngbin438 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eye.pngbin664 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eye_in_speech_bubble.pngbin698 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eyeglasses.pngbin577 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eyes.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/face_palm.pngbin1523 -> 0 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone1.pngbin1563 -> 0 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone2.pngbin1547 -> 0 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone3.pngbin1550 -> 0 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone4.pngbin1553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone5.pngbin1532 -> 0 bytes
-rw-r--r--app/assets/images/emoji/factory.pngbin936 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fallen_leaf.pngbin951 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family.pngbin1433 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mmb.pngbin1206 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mmbb.pngbin1349 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mmg.pngbin1361 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mmgb.pngbin1626 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mmgg.pngbin1448 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mwbb.pngbin1638 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mwg.pngbin1554 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mwgb.pngbin1837 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mwgg.pngbin1738 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_wwb.pngbin1155 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_wwbb.pngbin1289 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_wwg.pngbin1286 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_wwgb.pngbin1550 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_wwgg.pngbin1374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fast_forward.pngbin523 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fax.pngbin1188 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fearful.pngbin1002 -> 0 bytes
-rw-r--r--app/assets/images/emoji/feet.pngbin603 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fencer.pngbin1342 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ferris_wheel.pngbin2185 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ferry.pngbin528 -> 0 bytes
-rw-r--r--app/assets/images/emoji/field_hockey.pngbin947 -> 0 bytes
-rw-r--r--app/assets/images/emoji/file_cabinet.pngbin1420 -> 0 bytes
-rw-r--r--app/assets/images/emoji/file_folder.pngbin1445 -> 0 bytes
-rw-r--r--app/assets/images/emoji/film_frames.pngbin560 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed.pngbin1050 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone1.pngbin1047 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone2.pngbin1050 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone3.pngbin1050 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone4.pngbin1046 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone5.pngbin1050 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fire.pngbin1020 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fire_engine.pngbin1656 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fireworks.pngbin1364 -> 0 bytes
-rw-r--r--app/assets/images/emoji/first_place.pngbin1419 -> 0 bytes
-rw-r--r--app/assets/images/emoji/first_quarter_moon.pngbin1152 -> 0 bytes
-rw-r--r--app/assets/images/emoji/first_quarter_moon_with_face.pngbin1068 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fish.pngbin1080 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fish_cake.pngbin1245 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fishing_pole_and_fish.pngbin1442 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fist.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fist_tone1.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fist_tone2.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fist_tone3.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fist_tone4.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fist_tone5.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/five.pngbin577 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ac.pngbin1934 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ad.pngbin1285 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ae.pngbin544 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_af.pngbin942 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ag.pngbin913 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ai.pngbin1056 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_al.pngbin905 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_am.pngbin514 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ao.pngbin997 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_aq.pngbin657 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ar.pngbin975 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_as.pngbin1489 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_at.pngbin430 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_au.pngbin962 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_aw.pngbin709 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ax.pngbin496 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_az.pngbin709 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ba.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bb.pngbin789 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bd.pngbin490 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_be.pngbin444 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bf.pngbin717 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bg.pngbin513 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bh.pngbin593 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bi.pngbin795 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bj.pngbin554 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bl.pngbin1691 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_black.pngbin702 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bm.pngbin1374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bn.pngbin1355 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bo.pngbin1132 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bq.pngbin1144 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_br.pngbin819 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bs.pngbin448 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bt.pngbin1213 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bv.pngbin495 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bw.pngbin391 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_by.pngbin1120 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bz.pngbin1595 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ca.pngbin755 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cc.pngbin851 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cd.pngbin707 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cf.pngbin673 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cg.pngbin586 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ch.pngbin390 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ci.pngbin440 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ck.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cl.pngbin748 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cm.pngbin627 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cn.pngbin676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_co.pngbin524 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cp.pngbin443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cr.pngbin419 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cu.pngbin586 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cv.pngbin642 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cw.pngbin665 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cx.pngbin1142 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cy.pngbin830 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cz.pngbin600 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_de.pngbin502 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_dg.pngbin1911 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_dj.pngbin753 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_dk.pngbin450 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_dm.pngbin1075 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_do.pngbin1135 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_dz.pngbin734 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ea.pngbin1337 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ec.pngbin1431 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ee.pngbin512 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_eg.pngbin818 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_eh.pngbin742 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_er.pngbin1218 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_es.pngbin1337 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_et.pngbin947 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_eu.pngbin760 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_fi.pngbin487 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_fj.pngbin1381 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_fk.pngbin1558 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_fm.pngbin554 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_fo.pngbin495 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_fr.pngbin443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ga.pngbin512 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gb.pngbin919 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gd.pngbin1017 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ge.pngbin583 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gf.pngbin865 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gg.pngbin521 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gh.pngbin723 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gi.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gl.pngbin700 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gm.pngbin501 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gn.pngbin434 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gp.pngbin1587 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gq.pngbin1132 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gr.pngbin549 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gs.pngbin2115 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gt.pngbin1087 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gu.pngbin1045 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gw.pngbin705 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gy.pngbin690 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_hk.pngbin759 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_hm.pngbin1036 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_hn.pngbin513 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_hr.pngbin1411 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ht.pngbin1205 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_hu.pngbin513 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ic.pngbin1330 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_id.pngbin498 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ie.pngbin478 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_il.pngbin658 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_im.pngbin976 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_in.pngbin773 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_io.pngbin1911 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_iq.pngbin811 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ir.pngbin1036 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_is.pngbin491 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_it.pngbin472 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_je.pngbin956 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_jm.pngbin837 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_jo.pngbin740 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_jp.pngbin455 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ke.pngbin1160 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kg.pngbin1080 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kh.pngbin872 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ki.pngbin1369 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_km.pngbin783 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kn.pngbin1316 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kp.pngbin696 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kr.pngbin967 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kw.pngbin560 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ky.pngbin1671 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kz.pngbin1136 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_la.pngbin479 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lb.pngbin740 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lc.pngbin561 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_li.pngbin946 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lk.pngbin974 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lr.pngbin772 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ls.pngbin775 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lt.pngbin510 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lu.pngbin512 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lv.pngbin388 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ly.pngbin685 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ma.pngbin626 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mc.pngbin528 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_md.pngbin1170 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_me.pngbin1074 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mf.pngbin443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mg.pngbin556 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mh.pngbin1138 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mk.pngbin1023 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ml.pngbin440 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mm.pngbin937 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mn.pngbin698 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mo.pngbin792 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mp.pngbin1797 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mq.pngbin780 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mr.pngbin657 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ms.pngbin1477 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mt.pngbin799 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mu.pngbin544 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mv.pngbin598 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mw.pngbin825 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mx.pngbin951 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_my.pngbin775 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mz.pngbin1159 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_na.pngbin1249 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_nc.pngbin1148 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ne.pngbin593 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_nf.pngbin877 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ng.pngbin438 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ni.pngbin823 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_nl.pngbin499 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_no.pngbin484 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_np.pngbin802 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_nr.pngbin529 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_nu.pngbin1128 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_nz.pngbin1099 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_om.pngbin754 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pa.pngbin830 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pe.pngbin439 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pf.pngbin1091 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pg.pngbin1076 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ph.pngbin867 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pk.pngbin753 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pl.pngbin522 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pm.pngbin2314 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pn.pngbin1895 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pr.pngbin605 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ps.pngbin574 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pt.pngbin1055 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pw.pngbin475 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_py.pngbin1085 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_qa.pngbin657 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_re.pngbin837 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ro.pngbin441 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_rs.pngbin1237 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ru.pngbin496 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_rw.pngbin940 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sa.pngbin781 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sb.pngbin1102 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sc.pngbin1073 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sd.pngbin578 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_se.pngbin455 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sg.pngbin730 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sh.pngbin1369 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_si.pngbin1030 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sj.pngbin495 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sk.pngbin780 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sl.pngbin510 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sm.pngbin2000 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sn.pngbin621 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_so.pngbin609 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sr.pngbin650 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ss.pngbin722 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_st.pngbin562 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sv.pngbin1125 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sx.pngbin1195 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sy.pngbin696 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sz.pngbin1102 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ta.pngbin1907 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tc.pngbin1538 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_td.pngbin443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tf.pngbin857 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tg.pngbin790 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_th.pngbin421 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tj.pngbin906 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tk.pngbin835 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tl.pngbin849 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tm.pngbin1178 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tn.pngbin625 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_to.pngbin553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tr.pngbin576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tt.pngbin604 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tv.pngbin1120 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tw.pngbin761 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tz.pngbin1061 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ua.pngbin528 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ug.pngbin887 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_um.pngbin776 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_us.pngbin776 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_uy.pngbin966 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_uz.pngbin750 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_va.pngbin1331 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_vc.pngbin897 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ve.pngbin748 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_vg.pngbin1789 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_vi.pngbin1378 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_vn.pngbin583 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_vu.pngbin844 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_wf.pngbin443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_white.pngbin699 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ws.pngbin634 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_xk.pngbin722 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ye.pngbin507 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_yt.pngbin1623 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_za.pngbin676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_zm.pngbin881 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_zw.pngbin993 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flags.pngbin1722 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flashlight.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fleur-de-lis.pngbin632 -> 0 bytes
-rw-r--r--app/assets/images/emoji/floppy_disk.pngbin258 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flower_playing_cards.pngbin449 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flushed.pngbin1127 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fog.pngbin713 -> 0 bytes
-rw-r--r--app/assets/images/emoji/foggy.pngbin1069 -> 0 bytes
-rw-r--r--app/assets/images/emoji/football.pngbin956 -> 0 bytes
-rw-r--r--app/assets/images/emoji/footprints.pngbin621 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fork_and_knife.pngbin668 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fork_knife_plate.pngbin976 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fountain.pngbin1768 -> 0 bytes
-rw-r--r--app/assets/images/emoji/four.pngbin497 -> 0 bytes
-rw-r--r--app/assets/images/emoji/four_leaf_clover.pngbin1156 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fox.pngbin1556 -> 0 bytes
-rw-r--r--app/assets/images/emoji/frame_photo.pngbin514 -> 0 bytes
-rw-r--r--app/assets/images/emoji/free.pngbin370 -> 0 bytes
-rw-r--r--app/assets/images/emoji/french_bread.pngbin1551 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fried_shrimp.pngbin1241 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fries.pngbin1873 -> 0 bytes
-rw-r--r--app/assets/images/emoji/frog.pngbin897 -> 0 bytes
-rw-r--r--app/assets/images/emoji/frowning.pngbin633 -> 0 bytes
-rw-r--r--app/assets/images/emoji/frowning2.pngbin589 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fuelpump.pngbin864 -> 0 bytes
-rw-r--r--app/assets/images/emoji/full_moon.pngbin841 -> 0 bytes
-rw-r--r--app/assets/images/emoji/full_moon_with_face.pngbin1186 -> 0 bytes
-rw-r--r--app/assets/images/emoji/game_die.pngbin1136 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gay_pride_flag.pngbin2340 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gear.pngbin747 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gem.pngbin715 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gemini.pngbin547 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ghost.pngbin1465 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gift.pngbin1966 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gift_heart.pngbin1141 -> 0 bytes
-rw-r--r--app/assets/images/emoji/girl.pngbin1261 -> 0 bytes
-rw-r--r--app/assets/images/emoji/girl_tone1.pngbin1259 -> 0 bytes
-rw-r--r--app/assets/images/emoji/girl_tone2.pngbin1255 -> 0 bytes
-rw-r--r--app/assets/images/emoji/girl_tone3.pngbin1255 -> 0 bytes
-rw-r--r--app/assets/images/emoji/girl_tone4.pngbin1241 -> 0 bytes
-rw-r--r--app/assets/images/emoji/girl_tone5.pngbin1245 -> 0 bytes
-rw-r--r--app/assets/images/emoji/globe_with_meridians.pngbin796 -> 0 bytes
-rw-r--r--app/assets/images/emoji/goal.pngbin1242 -> 0 bytes
-rw-r--r--app/assets/images/emoji/goat.pngbin981 -> 0 bytes
-rw-r--r--app/assets/images/emoji/golf.pngbin823 -> 0 bytes
-rw-r--r--app/assets/images/emoji/golfer.pngbin1189 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gorilla.pngbin1090 -> 0 bytes
-rw-r--r--app/assets/images/emoji/grapes.pngbin1552 -> 0 bytes
-rw-r--r--app/assets/images/emoji/green_apple.pngbin656 -> 0 bytes
-rw-r--r--app/assets/images/emoji/green_book.pngbin1366 -> 0 bytes
-rw-r--r--app/assets/images/emoji/green_heart.pngbin435 -> 0 bytes
-rw-r--r--app/assets/images/emoji/grey_exclamation.pngbin354 -> 0 bytes
-rw-r--r--app/assets/images/emoji/grey_question.pngbin449 -> 0 bytes
-rw-r--r--app/assets/images/emoji/grimacing.pngbin694 -> 0 bytes
-rw-r--r--app/assets/images/emoji/grin.pngbin767 -> 0 bytes
-rw-r--r--app/assets/images/emoji/grinning.pngbin810 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guardsman.pngbin1140 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone1.pngbin1122 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone2.pngbin1160 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone3.pngbin1160 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone4.pngbin1157 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone5.pngbin1165 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guitar.pngbin1056 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gun.pngbin1859 -> 0 bytes
-rw-r--r--app/assets/images/emoji/haircut.pngbin1935 -> 0 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone1.pngbin1945 -> 0 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone2.pngbin1935 -> 0 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone3.pngbin1923 -> 0 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone4.pngbin1904 -> 0 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone5.pngbin1920 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hamburger.pngbin1973 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hammer.pngbin834 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hammer_pick.pngbin1068 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hamster.pngbin1279 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed.pngbin1081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone1.pngbin1081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone2.pngbin1081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone3.pngbin1081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone4.pngbin1081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone5.pngbin1081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handbag.pngbin1285 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handball.pngbin1634 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handball_tone1.pngbin1645 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handball_tone2.pngbin1628 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handball_tone3.pngbin1639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handball_tone4.pngbin1634 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handball_tone5.pngbin1606 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handshake.pngbin1366 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone1.pngbin1381 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone2.pngbin1381 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone3.pngbin1381 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone4.pngbin1381 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone5.pngbin1381 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hash.pngbin604 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hatched_chick.pngbin1174 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hatching_chick.pngbin1598 -> 0 bytes
-rw-r--r--app/assets/images/emoji/head_bandage.pngbin1199 -> 0 bytes
-rw-r--r--app/assets/images/emoji/headphones.pngbin1202 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hear_no_evil.pngbin1210 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heart.pngbin435 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heart_decoration.pngbin557 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heart_exclamation.pngbin471 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heart_eyes.pngbin1069 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heart_eyes_cat.pngbin1512 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heartbeat.pngbin699 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heartpulse.pngbin675 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hearts.pngbin449 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heavy_check_mark.pngbin438 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heavy_division_sign.pngbin204 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heavy_dollar_sign.pngbin429 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heavy_minus_sign.pngbin108 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heavy_multiplication_x.pngbin298 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heavy_plus_sign.pngbin115 -> 0 bytes
-rw-r--r--app/assets/images/emoji/helicopter.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/helmet_with_cross.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/herb.pngbin886 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hibiscus.pngbin1815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/high_brightness.pngbin474 -> 0 bytes
-rw-r--r--app/assets/images/emoji/high_heel.pngbin1008 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hockey.pngbin1010 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hole.pngbin1390 -> 0 bytes
-rw-r--r--app/assets/images/emoji/homes.pngbin981 -> 0 bytes
-rw-r--r--app/assets/images/emoji/honey_pot.pngbin1217 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse.pngbin1694 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse_racing.pngbin2096 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone1.pngbin2099 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone2.pngbin2103 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone3.pngbin2090 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone4.pngbin2090 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone5.pngbin2085 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hospital.pngbin530 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hot_pepper.pngbin677 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hotdog.pngbin1770 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hotel.pngbin1322 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hotsprings.pngbin733 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hourglass.pngbin800 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hourglass_flowing_sand.pngbin847 -> 0 bytes
-rw-r--r--app/assets/images/emoji/house.pngbin863 -> 0 bytes
-rw-r--r--app/assets/images/emoji/house_abandoned.pngbin1606 -> 0 bytes
-rw-r--r--app/assets/images/emoji/house_with_garden.pngbin1613 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hugging.pngbin1425 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hushed.pngbin634 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ice_cream.pngbin1779 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ice_skate.pngbin1574 -> 0 bytes
-rw-r--r--app/assets/images/emoji/icecream.pngbin1496 -> 0 bytes
-rw-r--r--app/assets/images/emoji/id.pngbin348 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ideograph_advantage.pngbin716 -> 0 bytes
-rw-r--r--app/assets/images/emoji/imp.pngbin1988 -> 0 bytes
-rw-r--r--app/assets/images/emoji/inbox_tray.pngbin1029 -> 0 bytes
-rw-r--r--app/assets/images/emoji/incoming_envelope.pngbin1129 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person.pngbin1580 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone1.pngbin1597 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone2.pngbin1590 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone3.pngbin1580 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone4.pngbin1572 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone5.pngbin1588 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_source.pngbin506 -> 0 bytes
-rw-r--r--app/assets/images/emoji/innocent.pngbin935 -> 0 bytes
-rw-r--r--app/assets/images/emoji/interrobang.pngbin601 -> 0 bytes
-rw-r--r--app/assets/images/emoji/iphone.pngbin695 -> 0 bytes
-rw-r--r--app/assets/images/emoji/island.pngbin1273 -> 0 bytes
-rw-r--r--app/assets/images/emoji/izakaya_lantern.pngbin1227 -> 0 bytes
-rw-r--r--app/assets/images/emoji/jack_o_lantern.pngbin2289 -> 0 bytes
-rw-r--r--app/assets/images/emoji/japan.pngbin539 -> 0 bytes
-rw-r--r--app/assets/images/emoji/japanese_castle.pngbin1404 -> 0 bytes
-rw-r--r--app/assets/images/emoji/japanese_goblin.pngbin1561 -> 0 bytes
-rw-r--r--app/assets/images/emoji/japanese_ogre.pngbin1864 -> 0 bytes
-rw-r--r--app/assets/images/emoji/jeans.pngbin1158 -> 0 bytes
-rw-r--r--app/assets/images/emoji/joy.pngbin1136 -> 0 bytes
-rw-r--r--app/assets/images/emoji/joy_cat.pngbin1633 -> 0 bytes
-rw-r--r--app/assets/images/emoji/joystick.pngbin1039 -> 0 bytes
-rw-r--r--app/assets/images/emoji/juggling.pngbin1165 -> 0 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone1.pngbin1171 -> 0 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone2.pngbin1160 -> 0 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone3.pngbin1170 -> 0 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone4.pngbin1167 -> 0 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone5.pngbin1161 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kaaba.pngbin1251 -> 0 bytes
-rw-r--r--app/assets/images/emoji/key.pngbin770 -> 0 bytes
-rw-r--r--app/assets/images/emoji/key2.pngbin593 -> 0 bytes
-rw-r--r--app/assets/images/emoji/keyboard.pngbin429 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kimono.pngbin1527 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kiss.pngbin842 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kiss_mm.pngbin1269 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kiss_ww.pngbin1149 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kissing.pngbin738 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kissing_cat.pngbin1468 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kissing_closed_eyes.pngbin888 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kissing_heart.pngbin843 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kissing_smiling_eyes.pngbin648 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kiwi.pngbin1892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/knife.pngbin616 -> 0 bytes
-rw-r--r--app/assets/images/emoji/koala.pngbin1428 -> 0 bytes
-rw-r--r--app/assets/images/emoji/koko.pngbin266 -> 0 bytes
-rw-r--r--app/assets/images/emoji/label.pngbin669 -> 0 bytes
-rw-r--r--app/assets/images/emoji/large_blue_circle.pngbin371 -> 0 bytes
-rw-r--r--app/assets/images/emoji/large_blue_diamond.pngbin245 -> 0 bytes
-rw-r--r--app/assets/images/emoji/large_orange_diamond.pngbin248 -> 0 bytes
-rw-r--r--app/assets/images/emoji/last_quarter_moon.pngbin1180 -> 0 bytes
-rw-r--r--app/assets/images/emoji/last_quarter_moon_with_face.pngbin1030 -> 0 bytes
-rw-r--r--app/assets/images/emoji/laughing.pngbin901 -> 0 bytes
-rw-r--r--app/assets/images/emoji/leaves.pngbin993 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ledger.pngbin1528 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist.pngbin972 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone1.pngbin960 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone2.pngbin972 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone3.pngbin960 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone4.pngbin960 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone5.pngbin976 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_luggage.pngbin576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_right_arrow.pngbin495 -> 0 bytes
-rw-r--r--app/assets/images/emoji/leftwards_arrow_with_hook.pngbin643 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lemon.pngbin1033 -> 0 bytes
-rw-r--r--app/assets/images/emoji/leo.pngbin745 -> 0 bytes
-rw-r--r--app/assets/images/emoji/leopard.pngbin2222 -> 0 bytes
-rw-r--r--app/assets/images/emoji/level_slider.pngbin454 -> 0 bytes
-rw-r--r--app/assets/images/emoji/levitate.pngbin914 -> 0 bytes
-rw-r--r--app/assets/images/emoji/libra.pngbin657 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lifter.pngbin1356 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone1.pngbin1346 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone2.pngbin1347 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone3.pngbin1339 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone4.pngbin1343 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone5.pngbin1337 -> 0 bytes
-rw-r--r--app/assets/images/emoji/light_rail.pngbin902 -> 0 bytes
-rw-r--r--app/assets/images/emoji/link.pngbin477 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lion_face.pngbin1728 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lips.pngbin599 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lipstick.pngbin549 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lizard.pngbin1709 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lock.pngbin986 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lock_with_ink_pen.pngbin1123 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lollipop.pngbin2164 -> 0 bytes
-rw-r--r--app/assets/images/emoji/loop.pngbin550 -> 0 bytes
-rw-r--r--app/assets/images/emoji/loud_sound.pngbin977 -> 0 bytes
-rw-r--r--app/assets/images/emoji/loudspeaker.pngbin1316 -> 0 bytes
-rw-r--r--app/assets/images/emoji/love_hotel.pngbin372 -> 0 bytes
-rw-r--r--app/assets/images/emoji/love_letter.pngbin923 -> 0 bytes
-rw-r--r--app/assets/images/emoji/low_brightness.pngbin431 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lying_face.pngbin1103 -> 0 bytes
-rw-r--r--app/assets/images/emoji/m.pngbin500 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mag.pngbin1240 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mag_right.pngbin1251 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mahjong.pngbin951 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mailbox.pngbin1166 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mailbox_closed.pngbin1192 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mailbox_with_mail.pngbin1307 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mailbox_with_no_mail.pngbin960 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man.pngbin1092 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_dancing.pngbin1400 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone1.pngbin1404 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone2.pngbin1402 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone3.pngbin1409 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone4.pngbin1421 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone5.pngbin1418 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo.pngbin1307 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone1.pngbin1307 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone2.pngbin1307 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone3.pngbin1307 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone4.pngbin1307 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone5.pngbin1302 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_tone1.pngbin1069 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_tone2.pngbin1069 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_tone3.pngbin1069 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_tone4.pngbin1069 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_tone5.pngbin1087 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao.pngbin1339 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone1.pngbin1328 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone2.pngbin1332 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone3.pngbin1329 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone4.pngbin1325 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone5.pngbin1337 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban.pngbin1618 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone1.pngbin1584 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone2.pngbin1588 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone3.pngbin1584 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone4.pngbin1583 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone5.pngbin1605 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mans_shoe.pngbin1649 -> 0 bytes
-rw-r--r--app/assets/images/emoji/map.pngbin2352 -> 0 bytes
-rw-r--r--app/assets/images/emoji/maple_leaf.pngbin1117 -> 0 bytes
-rw-r--r--app/assets/images/emoji/martial_arts_uniform.pngbin1412 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mask.pngbin1322 -> 0 bytes
-rw-r--r--app/assets/images/emoji/massage.pngbin1571 -> 0 bytes
-rw-r--r--app/assets/images/emoji/massage_tone1.pngbin1578 -> 0 bytes
-rw-r--r--app/assets/images/emoji/massage_tone2.pngbin1565 -> 0 bytes
-rw-r--r--app/assets/images/emoji/massage_tone3.pngbin1553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/massage_tone4.pngbin1546 -> 0 bytes
-rw-r--r--app/assets/images/emoji/massage_tone5.pngbin1557 -> 0 bytes
-rw-r--r--app/assets/images/emoji/meat_on_bone.pngbin1465 -> 0 bytes
-rw-r--r--app/assets/images/emoji/medal.pngbin1700 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mega.pngbin1751 -> 0 bytes
-rw-r--r--app/assets/images/emoji/melon.pngbin2005 -> 0 bytes
-rw-r--r--app/assets/images/emoji/menorah.pngbin1279 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mens.pngbin561 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metal.pngbin894 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metal_tone1.pngbin894 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metal_tone2.pngbin888 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metal_tone3.pngbin894 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metal_tone4.pngbin888 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metal_tone5.pngbin894 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metro.pngbin1020 -> 0 bytes
-rw-r--r--app/assets/images/emoji/microphone.pngbin1165 -> 0 bytes
-rw-r--r--app/assets/images/emoji/microphone2.pngbin839 -> 0 bytes
-rw-r--r--app/assets/images/emoji/microscope.pngbin1113 -> 0 bytes
-rw-r--r--app/assets/images/emoji/middle_finger.pngbin893 -> 0 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone1.pngbin892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone2.pngbin892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone3.pngbin892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone4.pngbin892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone5.pngbin892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/military_medal.pngbin949 -> 0 bytes
-rw-r--r--app/assets/images/emoji/milk.pngbin1224 -> 0 bytes
-rw-r--r--app/assets/images/emoji/milky_way.pngbin622 -> 0 bytes
-rw-r--r--app/assets/images/emoji/minibus.pngbin1256 -> 0 bytes
-rw-r--r--app/assets/images/emoji/minidisc.pngbin522 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mobile_phone_off.pngbin621 -> 0 bytes
-rw-r--r--app/assets/images/emoji/money_mouth.pngbin967 -> 0 bytes
-rw-r--r--app/assets/images/emoji/money_with_wings.pngbin2327 -> 0 bytes
-rw-r--r--app/assets/images/emoji/moneybag.pngbin2310 -> 0 bytes
-rw-r--r--app/assets/images/emoji/monkey.pngbin1348 -> 0 bytes
-rw-r--r--app/assets/images/emoji/monkey_face.pngbin1022 -> 0 bytes
-rw-r--r--app/assets/images/emoji/monorail.pngbin1068 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mortar_board.pngbin710 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mosque.pngbin984 -> 0 bytes
-rw-r--r--app/assets/images/emoji/motor_scooter.pngbin1207 -> 0 bytes
-rw-r--r--app/assets/images/emoji/motorboat.pngbin990 -> 0 bytes
-rw-r--r--app/assets/images/emoji/motorcycle.pngbin2081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/motorway.pngbin1102 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mount_fuji.pngbin881 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain.pngbin1409 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist.pngbin2288 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone1.pngbin2294 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone2.pngbin2298 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone3.pngbin2284 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone4.pngbin2288 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone5.pngbin2281 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_cableway.pngbin811 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_railway.pngbin1317 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_snow.pngbin1193 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mouse.pngbin1245 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mouse2.pngbin1324 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mouse_three_button.pngbin934 -> 0 bytes
-rw-r--r--app/assets/images/emoji/movie_camera.pngbin576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/moyai.pngbin1593 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus.pngbin3338 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone1.pngbin1999 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone2.pngbin2006 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone3.pngbin2017 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone4.pngbin2016 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone5.pngbin2016 -> 0 bytes
-rw-r--r--app/assets/images/emoji/muscle.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone1.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone2.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone3.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone4.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone5.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mushroom.pngbin1024 -> 0 bytes
-rw-r--r--app/assets/images/emoji/musical_keyboard.pngbin1695 -> 0 bytes
-rw-r--r--app/assets/images/emoji/musical_note.pngbin419 -> 0 bytes
-rw-r--r--app/assets/images/emoji/musical_score.pngbin1289 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mute.pngbin823 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nail_care.pngbin1639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone1.pngbin1712 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone2.pngbin1711 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone3.pngbin1727 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone4.pngbin1728 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone5.pngbin1716 -> 0 bytes
-rw-r--r--app/assets/images/emoji/name_badge.pngbin632 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nauseated_face.pngbin965 -> 0 bytes
-rw-r--r--app/assets/images/emoji/necktie.pngbin995 -> 0 bytes
-rw-r--r--app/assets/images/emoji/negative_squared_cross_mark.pngbin370 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nerd.pngbin975 -> 0 bytes
-rw-r--r--app/assets/images/emoji/neutral_face.pngbin517 -> 0 bytes
-rw-r--r--app/assets/images/emoji/new.pngbin486 -> 0 bytes
-rw-r--r--app/assets/images/emoji/new_moon.pngbin829 -> 0 bytes
-rw-r--r--app/assets/images/emoji/new_moon_with_face.pngbin975 -> 0 bytes
-rw-r--r--app/assets/images/emoji/newspaper.pngbin1178 -> 0 bytes
-rw-r--r--app/assets/images/emoji/newspaper2.pngbin1046 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ng.pngbin445 -> 0 bytes
-rw-r--r--app/assets/images/emoji/night_with_stars.pngbin835 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nine.pngbin607 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_bell.pngbin823 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_bicycles.pngbin998 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_entry.pngbin377 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_entry_sign.pngbin555 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_good.pngbin1750 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone1.pngbin1767 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone2.pngbin1756 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone3.pngbin1766 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone4.pngbin1782 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone5.pngbin1784 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_mobile_phones.pngbin790 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_mouth.pngbin465 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_pedestrians.pngbin875 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_smoking.pngbin1136 -> 0 bytes
-rw-r--r--app/assets/images/emoji/non-potable_water.pngbin827 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nose.pngbin703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nose_tone1.pngbin703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nose_tone2.pngbin703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nose_tone3.pngbin703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nose_tone4.pngbin703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nose_tone5.pngbin703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/notebook.pngbin1215 -> 0 bytes
-rw-r--r--app/assets/images/emoji/notebook_with_decorative_cover.pngbin1782 -> 0 bytes
-rw-r--r--app/assets/images/emoji/notepad_spiral.pngbin1377 -> 0 bytes
-rw-r--r--app/assets/images/emoji/notes.pngbin501 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nut_and_bolt.pngbin899 -> 0 bytes
-rw-r--r--app/assets/images/emoji/o.pngbin475 -> 0 bytes
-rw-r--r--app/assets/images/emoji/o2.pngbin425 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ocean.pngbin1018 -> 0 bytes
-rw-r--r--app/assets/images/emoji/octagonal_sign.pngbin260 -> 0 bytes
-rw-r--r--app/assets/images/emoji/octopus.pngbin1188 -> 0 bytes
-rw-r--r--app/assets/images/emoji/oden.pngbin794 -> 0 bytes
-rw-r--r--app/assets/images/emoji/office.pngbin524 -> 0 bytes
-rw-r--r--app/assets/images/emoji/oil.pngbin674 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok.pngbin511 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_hand.pngbin979 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone1.pngbin979 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone2.pngbin979 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone3.pngbin979 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone4.pngbin979 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone5.pngbin979 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_woman.pngbin1696 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone1.pngbin1696 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone2.pngbin1694 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone3.pngbin1675 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone4.pngbin1684 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone5.pngbin1696 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_man.pngbin1253 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone1.pngbin1253 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone2.pngbin1253 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone3.pngbin1253 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone4.pngbin1254 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone5.pngbin1254 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_woman.pngbin1472 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone1.pngbin1562 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone2.pngbin1564 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone3.pngbin1555 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone4.pngbin1562 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone5.pngbin1544 -> 0 bytes
-rw-r--r--app/assets/images/emoji/om_symbol.pngbin773 -> 0 bytes
-rw-r--r--app/assets/images/emoji/on.pngbin459 -> 0 bytes
-rw-r--r--app/assets/images/emoji/oncoming_automobile.pngbin1238 -> 0 bytes
-rw-r--r--app/assets/images/emoji/oncoming_bus.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/oncoming_police_car.pngbin1547 -> 0 bytes
-rw-r--r--app/assets/images/emoji/oncoming_taxi.pngbin1405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/one.pngbin442 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_file_folder.pngbin755 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_hands.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone1.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone2.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone3.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone4.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone5.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_mouth.pngbin575 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ophiuchus.pngbin723 -> 0 bytes
-rw-r--r--app/assets/images/emoji/orange_book.pngbin1329 -> 0 bytes
-rw-r--r--app/assets/images/emoji/orthodox_cross.pngbin239 -> 0 bytes
-rw-r--r--app/assets/images/emoji/outbox_tray.pngbin1002 -> 0 bytes
-rw-r--r--app/assets/images/emoji/owl.pngbin2045 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ox.pngbin1436 -> 0 bytes
-rw-r--r--app/assets/images/emoji/package.pngbin950 -> 0 bytes
-rw-r--r--app/assets/images/emoji/page_facing_up.pngbin1110 -> 0 bytes
-rw-r--r--app/assets/images/emoji/page_with_curl.pngbin1157 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pager.pngbin553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/paintbrush.pngbin950 -> 0 bytes
-rw-r--r--app/assets/images/emoji/palm_tree.pngbin1450 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pancakes.pngbin3661 -> 0 bytes
-rw-r--r--app/assets/images/emoji/panda_face.pngbin1478 -> 0 bytes
-rw-r--r--app/assets/images/emoji/paperclip.pngbin439 -> 0 bytes
-rw-r--r--app/assets/images/emoji/paperclips.pngbin642 -> 0 bytes
-rw-r--r--app/assets/images/emoji/park.pngbin929 -> 0 bytes
-rw-r--r--app/assets/images/emoji/parking.pngbin385 -> 0 bytes
-rw-r--r--app/assets/images/emoji/part_alternation_mark.pngbin521 -> 0 bytes
-rw-r--r--app/assets/images/emoji/partly_sunny.pngbin977 -> 0 bytes
-rw-r--r--app/assets/images/emoji/passport_control.pngbin683 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pause_button.pngbin395 -> 0 bytes
-rw-r--r--app/assets/images/emoji/peace.pngbin933 -> 0 bytes
-rw-r--r--app/assets/images/emoji/peach.pngbin1189 -> 0 bytes
-rw-r--r--app/assets/images/emoji/peanuts.pngbin3266 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pear.pngbin747 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pen_ballpoint.pngbin696 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pen_fountain.pngbin623 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pencil.pngbin1624 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pencil2.pngbin654 -> 0 bytes
-rw-r--r--app/assets/images/emoji/penguin.pngbin1034 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pensive.pngbin718 -> 0 bytes
-rw-r--r--app/assets/images/emoji/performing_arts.pngbin1971 -> 0 bytes
-rw-r--r--app/assets/images/emoji/persevere.pngbin891 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_frowning.pngbin1148 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone1.pngbin1141 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone2.pngbin1141 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone3.pngbin1141 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone4.pngbin1109 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone5.pngbin1114 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair.pngbin1205 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone1.pngbin1181 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone2.pngbin1181 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone3.pngbin1181 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone4.pngbin1189 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone5.pngbin1214 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face.pngbin1297 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone1.pngbin1309 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone2.pngbin1292 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone3.pngbin1305 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone4.pngbin1296 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone5.pngbin1303 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pick.pngbin1023 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pig.pngbin1138 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pig2.pngbin1548 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pig_nose.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pill.pngbin442 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pineapple.pngbin1642 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ping_pong.pngbin823 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pisces.pngbin678 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pizza.pngbin2008 -> 0 bytes
-rw-r--r--app/assets/images/emoji/place_of_worship.pngbin487 -> 0 bytes
-rw-r--r--app/assets/images/emoji/play_pause.pngbin509 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_down.pngbin853 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone1.pngbin856 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone2.pngbin856 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone3.pngbin858 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone4.pngbin856 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone5.pngbin856 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_left.pngbin825 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone1.pngbin832 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone2.pngbin830 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone3.pngbin830 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone4.pngbin830 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone5.pngbin832 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_right.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone1.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone2.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone3.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone4.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone5.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up.pngbin819 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_2.pngbin822 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone1.pngbin822 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone2.pngbin822 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone3.pngbin871 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone4.pngbin822 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone5.pngbin822 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone1.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone2.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone3.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone4.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone5.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/police_car.pngbin1431 -> 0 bytes
-rw-r--r--app/assets/images/emoji/poodle.pngbin1531 -> 0 bytes
-rw-r--r--app/assets/images/emoji/poop.pngbin1273 -> 0 bytes
-rw-r--r--app/assets/images/emoji/popcorn.pngbin1843 -> 0 bytes
-rw-r--r--app/assets/images/emoji/post_office.pngbin676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/postal_horn.pngbin809 -> 0 bytes
-rw-r--r--app/assets/images/emoji/postbox.pngbin1077 -> 0 bytes
-rw-r--r--app/assets/images/emoji/potable_water.pngbin633 -> 0 bytes
-rw-r--r--app/assets/images/emoji/potato.pngbin1246 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pouch.pngbin1259 -> 0 bytes
-rw-r--r--app/assets/images/emoji/poultry_leg.pngbin925 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pound.pngbin452 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pouting_cat.pngbin1675 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pray.pngbin1122 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pray_tone1.pngbin1131 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pray_tone2.pngbin1134 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pray_tone3.pngbin1137 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pray_tone4.pngbin1126 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pray_tone5.pngbin1117 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prayer_beads.pngbin1059 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman.pngbin1252 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone1.pngbin1255 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone2.pngbin1246 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone3.pngbin1237 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone4.pngbin1246 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone5.pngbin1235 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prince.pngbin1616 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prince_tone1.pngbin1618 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prince_tone2.pngbin1621 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prince_tone3.pngbin1619 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prince_tone4.pngbin1619 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prince_tone5.pngbin1616 -> 0 bytes
-rw-r--r--app/assets/images/emoji/princess.pngbin1812 -> 0 bytes
-rw-r--r--app/assets/images/emoji/princess_tone1.pngbin1812 -> 0 bytes
-rw-r--r--app/assets/images/emoji/princess_tone2.pngbin1805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/princess_tone3.pngbin1805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/princess_tone4.pngbin1813 -> 0 bytes
-rw-r--r--app/assets/images/emoji/princess_tone5.pngbin1812 -> 0 bytes
-rw-r--r--app/assets/images/emoji/printer.pngbin926 -> 0 bytes
-rw-r--r--app/assets/images/emoji/projector.pngbin943 -> 0 bytes
-rw-r--r--app/assets/images/emoji/punch.pngbin838 -> 0 bytes
-rw-r--r--app/assets/images/emoji/punch_tone1.pngbin838 -> 0 bytes
-rw-r--r--app/assets/images/emoji/punch_tone2.pngbin838 -> 0 bytes
-rw-r--r--app/assets/images/emoji/punch_tone3.pngbin838 -> 0 bytes
-rw-r--r--app/assets/images/emoji/punch_tone4.pngbin838 -> 0 bytes
-rw-r--r--app/assets/images/emoji/punch_tone5.pngbin838 -> 0 bytes
-rw-r--r--app/assets/images/emoji/purple_heart.pngbin435 -> 0 bytes
-rw-r--r--app/assets/images/emoji/purse.pngbin1558 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pushpin.pngbin640 -> 0 bytes
-rw-r--r--app/assets/images/emoji/put_litter_in_its_place.pngbin650 -> 0 bytes
-rw-r--r--app/assets/images/emoji/question.pngbin449 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rabbit.pngbin1660 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rabbit2.pngbin1805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/race_car.pngbin2140 -> 0 bytes
-rw-r--r--app/assets/images/emoji/racehorse.pngbin1401 -> 0 bytes
-rw-r--r--app/assets/images/emoji/radio.pngbin851 -> 0 bytes
-rw-r--r--app/assets/images/emoji/radio_button.pngbin674 -> 0 bytes
-rw-r--r--app/assets/images/emoji/radioactive.pngbin858 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rage.pngbin845 -> 0 bytes
-rw-r--r--app/assets/images/emoji/railway_car.pngbin847 -> 0 bytes
-rw-r--r--app/assets/images/emoji/railway_track.pngbin1550 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rainbow.pngbin1299 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone1.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone2.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone3.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone4.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone5.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hand.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone1.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone2.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone3.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone4.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone5.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hands.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone1.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone2.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone3.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone4.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone5.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raising_hand.pngbin1664 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone1.pngbin1678 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone2.pngbin1665 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone3.pngbin1657 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone4.pngbin1657 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone5.pngbin1661 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ram.pngbin1951 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ramen.pngbin1992 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rat.pngbin1193 -> 0 bytes
-rw-r--r--app/assets/images/emoji/record_button.pngbin475 -> 0 bytes
-rw-r--r--app/assets/images/emoji/recycle.pngbin914 -> 0 bytes
-rw-r--r--app/assets/images/emoji/red_car.pngbin1065 -> 0 bytes
-rw-r--r--app/assets/images/emoji/red_circle.pngbin374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/registered.pngbin547 -> 0 bytes
-rw-r--r--app/assets/images/emoji/relaxed.pngbin636 -> 0 bytes
-rw-r--r--app/assets/images/emoji/relieved.pngbin785 -> 0 bytes
-rw-r--r--app/assets/images/emoji/reminder_ribbon.pngbin921 -> 0 bytes
-rw-r--r--app/assets/images/emoji/repeat.pngbin644 -> 0 bytes
-rw-r--r--app/assets/images/emoji/repeat_one.pngbin688 -> 0 bytes
-rw-r--r--app/assets/images/emoji/restroom.pngbin676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/revolving_hearts.pngbin920 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rewind.pngbin523 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rhino.pngbin1558 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ribbon.pngbin968 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rice.pngbin1195 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rice_ball.pngbin1091 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rice_cracker.pngbin1443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rice_scene.pngbin1349 -> 0 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist.pngbin975 -> 0 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone1.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone2.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone3.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone4.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone5.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ring.pngbin1113 -> 0 bytes
-rw-r--r--app/assets/images/emoji/robot.pngbin1228 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rocket.pngbin1639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rofl.pngbin1760 -> 0 bytes
-rw-r--r--app/assets/images/emoji/roller_coaster.pngbin1723 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rolling_eyes.pngbin743 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rooster.pngbin1333 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rose.pngbin1182 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rosette.pngbin1023 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rotating_light.pngbin1969 -> 0 bytes
-rw-r--r--app/assets/images/emoji/round_pushpin.pngbin455 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rowboat.pngbin1963 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone1.pngbin1971 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone2.pngbin1972 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone3.pngbin1967 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone4.pngbin1974 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone5.pngbin1971 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rugby_football.pngbin1618 -> 0 bytes
-rw-r--r--app/assets/images/emoji/runner.pngbin1161 -> 0 bytes
-rw-r--r--app/assets/images/emoji/runner_tone1.pngbin1163 -> 0 bytes
-rw-r--r--app/assets/images/emoji/runner_tone2.pngbin1162 -> 0 bytes
-rw-r--r--app/assets/images/emoji/runner_tone3.pngbin1151 -> 0 bytes
-rw-r--r--app/assets/images/emoji/runner_tone4.pngbin1156 -> 0 bytes
-rw-r--r--app/assets/images/emoji/runner_tone5.pngbin1145 -> 0 bytes
-rw-r--r--app/assets/images/emoji/running_shirt_with_sash.pngbin784 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sa.pngbin420 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sagittarius.pngbin602 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sailboat.pngbin1274 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sake.pngbin826 -> 0 bytes
-rw-r--r--app/assets/images/emoji/salad.pngbin2398 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sandal.pngbin1180 -> 0 bytes
-rw-r--r--app/assets/images/emoji/santa.pngbin1585 -> 0 bytes
-rw-r--r--app/assets/images/emoji/santa_tone1.pngbin1585 -> 0 bytes
-rw-r--r--app/assets/images/emoji/santa_tone2.pngbin1578 -> 0 bytes
-rw-r--r--app/assets/images/emoji/santa_tone3.pngbin1578 -> 0 bytes
-rw-r--r--app/assets/images/emoji/santa_tone4.pngbin1578 -> 0 bytes
-rw-r--r--app/assets/images/emoji/santa_tone5.pngbin1578 -> 0 bytes
-rw-r--r--app/assets/images/emoji/satellite.pngbin1173 -> 0 bytes
-rw-r--r--app/assets/images/emoji/satellite_orbital.pngbin762 -> 0 bytes
-rw-r--r--app/assets/images/emoji/saxophone.pngbin1442 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scales.pngbin1181 -> 0 bytes
-rw-r--r--app/assets/images/emoji/school.pngbin1234 -> 0 bytes
-rw-r--r--app/assets/images/emoji/school_satchel.pngbin1490 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scissors.pngbin937 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scooter.pngbin1228 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scorpion.pngbin1503 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scorpius.pngbin612 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scream.pngbin1583 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scream_cat.pngbin2120 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scroll.pngbin989 -> 0 bytes
-rw-r--r--app/assets/images/emoji/seat.pngbin884 -> 0 bytes
-rw-r--r--app/assets/images/emoji/second_place.pngbin1511 -> 0 bytes
-rw-r--r--app/assets/images/emoji/secret.pngbin857 -> 0 bytes
-rw-r--r--app/assets/images/emoji/see_no_evil.pngbin1227 -> 0 bytes
-rw-r--r--app/assets/images/emoji/seedling.pngbin749 -> 0 bytes
-rw-r--r--app/assets/images/emoji/selfie.pngbin1160 -> 0 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone1.pngbin1166 -> 0 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone2.pngbin1167 -> 0 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone3.pngbin1154 -> 0 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone4.pngbin1153 -> 0 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone5.pngbin1148 -> 0 bytes
-rw-r--r--app/assets/images/emoji/seven.pngbin522 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shallow_pan_of_food.pngbin1738 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shamrock.pngbin1023 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shark.pngbin1811 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shaved_ice.pngbin997 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sheep.pngbin1372 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shell.pngbin1497 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shield.pngbin1602 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shinto_shrine.pngbin579 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ship.pngbin1405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shirt.pngbin670 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shopping_bags.pngbin1234 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shopping_cart.pngbin1072 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shower.pngbin2537 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrimp.pngbin1376 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrug.pngbin1671 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone1.pngbin1676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone2.pngbin1671 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone3.pngbin1675 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone4.pngbin1641 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone5.pngbin1634 -> 0 bytes
-rw-r--r--app/assets/images/emoji/signal_strength.pngbin445 -> 0 bytes
-rw-r--r--app/assets/images/emoji/six.pngbin612 -> 0 bytes
-rw-r--r--app/assets/images/emoji/six_pointed_star.pngbin540 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ski.pngbin1762 -> 0 bytes
-rw-r--r--app/assets/images/emoji/skier.pngbin1539 -> 0 bytes
-rw-r--r--app/assets/images/emoji/skull.pngbin628 -> 0 bytes
-rw-r--r--app/assets/images/emoji/skull_crossbones.pngbin726 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sleeping.pngbin1075 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sleeping_accommodation.pngbin926 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sleepy.pngbin1185 -> 0 bytes
-rw-r--r--app/assets/images/emoji/slight_frown.pngbin580 -> 0 bytes
-rw-r--r--app/assets/images/emoji/slight_smile.pngbin600 -> 0 bytes
-rw-r--r--app/assets/images/emoji/slot_machine.pngbin1648 -> 0 bytes
-rw-r--r--app/assets/images/emoji/small_blue_diamond.pngbin191 -> 0 bytes
-rw-r--r--app/assets/images/emoji/small_orange_diamond.pngbin194 -> 0 bytes
-rw-r--r--app/assets/images/emoji/small_red_triangle.pngbin273 -> 0 bytes
-rw-r--r--app/assets/images/emoji/small_red_triangle_down.pngbin291 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smile.pngbin737 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smile_cat.pngbin1405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smiley.pngbin686 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smiley_cat.pngbin1669 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smiling_imp.pngbin1078 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smirk.pngbin775 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smirk_cat.pngbin1663 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smoking.pngbin417 -> 0 bytes
-rw-r--r--app/assets/images/emoji/snail.pngbin1731 -> 0 bytes
-rw-r--r--app/assets/images/emoji/snake.pngbin1575 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sneezing_face.pngbin1289 -> 0 bytes
-rw-r--r--app/assets/images/emoji/snowboarder.pngbin2020 -> 0 bytes
-rw-r--r--app/assets/images/emoji/snowflake.pngbin691 -> 0 bytes
-rw-r--r--app/assets/images/emoji/snowman.pngbin1481 -> 0 bytes
-rw-r--r--app/assets/images/emoji/snowman2.pngbin2176 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sob.pngbin1236 -> 0 bytes
-rw-r--r--app/assets/images/emoji/soccer.pngbin1034 -> 0 bytes
-rw-r--r--app/assets/images/emoji/soon.pngbin483 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sos.pngbin604 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sound.pngbin690 -> 0 bytes
-rw-r--r--app/assets/images/emoji/space_invader.pngbin1325 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spades.pngbin454 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spaghetti.pngbin1796 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sparkle.pngbin663 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sparkler.pngbin910 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sparkles.pngbin651 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sparkling_heart.pngbin821 -> 0 bytes
-rw-r--r--app/assets/images/emoji/speak_no_evil.pngbin1497 -> 0 bytes
-rw-r--r--app/assets/images/emoji/speaker.pngbin575 -> 0 bytes
-rw-r--r--app/assets/images/emoji/speaking_head.pngbin531 -> 0 bytes
-rw-r--r--app/assets/images/emoji/speech_balloon.pngbin384 -> 0 bytes
-rw-r--r--app/assets/images/emoji/speech_left.pngbin390 -> 0 bytes
-rw-r--r--app/assets/images/emoji/speedboat.pngbin1255 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spider.pngbin1724 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spider_web.pngbin929 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spoon.pngbin700 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spy.pngbin1650 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spy_tone1.pngbin1639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spy_tone2.pngbin1632 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spy_tone3.pngbin1645 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spy_tone4.pngbin1639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spy_tone5.pngbin1639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/squid.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stadium.pngbin1515 -> 0 bytes
-rw-r--r--app/assets/images/emoji/star.pngbin456 -> 0 bytes
-rw-r--r--app/assets/images/emoji/star2.pngbin732 -> 0 bytes
-rw-r--r--app/assets/images/emoji/star_and_crescent.pngbin490 -> 0 bytes
-rw-r--r--app/assets/images/emoji/star_of_david.pngbin491 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stars.pngbin1048 -> 0 bytes
-rw-r--r--app/assets/images/emoji/station.pngbin1336 -> 0 bytes
-rw-r--r--app/assets/images/emoji/statue_of_liberty.pngbin1145 -> 0 bytes
-rw-r--r--app/assets/images/emoji/steam_locomotive.pngbin1736 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stew.pngbin1960 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stop_button.pngbin385 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stopwatch.pngbin1329 -> 0 bytes
-rw-r--r--app/assets/images/emoji/straight_ruler.pngbin1406 -> 0 bytes
-rw-r--r--app/assets/images/emoji/strawberry.pngbin1206 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stuck_out_tongue.pngbin752 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stuck_out_tongue_closed_eyes.pngbin867 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stuck_out_tongue_winking_eye.pngbin1061 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stuffed_flatbread.pngbin2160 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sun_with_face.pngbin741 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sunflower.pngbin1915 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sunglasses.pngbin824 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sunny.pngbin746 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sunrise.pngbin812 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sunrise_over_mountains.pngbin1576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/surfer.pngbin1777 -> 0 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone1.pngbin1781 -> 0 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone2.pngbin1769 -> 0 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone3.pngbin1777 -> 0 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone4.pngbin1784 -> 0 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone5.pngbin1782 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sushi.pngbin2101 -> 0 bytes
-rw-r--r--app/assets/images/emoji/suspension_railway.pngbin927 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sweat.pngbin861 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sweat_drops.pngbin549 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sweat_smile.pngbin851 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sweet_potato.pngbin951 -> 0 bytes
-rw-r--r--app/assets/images/emoji/swimmer.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone1.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone2.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone3.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone4.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone5.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/symbols.pngbin746 -> 0 bytes
-rw-r--r--app/assets/images/emoji/synagogue.pngbin1309 -> 0 bytes
-rw-r--r--app/assets/images/emoji/syringe.pngbin737 -> 0 bytes
-rw-r--r--app/assets/images/emoji/taco.pngbin3045 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tada.pngbin1778 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tanabata_tree.pngbin1479 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tangerine.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/taurus.pngbin701 -> 0 bytes
-rw-r--r--app/assets/images/emoji/taxi.pngbin1230 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tea.pngbin1297 -> 0 bytes
-rw-r--r--app/assets/images/emoji/telephone.pngbin1760 -> 0 bytes
-rw-r--r--app/assets/images/emoji/telephone_receiver.pngbin941 -> 0 bytes
-rw-r--r--app/assets/images/emoji/telescope.pngbin1256 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ten.pngbin621 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tennis.pngbin1561 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tent.pngbin1684 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thermometer.pngbin759 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thermometer_face.pngbin1503 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thinking.pngbin1345 -> 0 bytes
-rw-r--r--app/assets/images/emoji/third_place.pngbin1529 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thought_balloon.pngbin489 -> 0 bytes
-rw-r--r--app/assets/images/emoji/three.pngbin602 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone1.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone2.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone3.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone4.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone5.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsup.pngbin814 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone1.pngbin814 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone2.pngbin814 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone3.pngbin814 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone4.pngbin814 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone5.pngbin814 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thunder_cloud_rain.pngbin1020 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ticket.pngbin763 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tickets.pngbin1750 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tiger.pngbin2104 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tiger2.pngbin2623 -> 0 bytes
-rw-r--r--app/assets/images/emoji/timer.pngbin1897 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tired_face.pngbin1126 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tm.pngbin300 -> 0 bytes
-rw-r--r--app/assets/images/emoji/toilet.pngbin726 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tokyo_tower.pngbin765 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tomato.pngbin1055 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tone1.pngbin372 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tone2.pngbin372 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tone3.pngbin375 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tone4.pngbin374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tone5.pngbin374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tongue.pngbin599 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tools.pngbin1225 -> 0 bytes
-rw-r--r--app/assets/images/emoji/top.pngbin389 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tophat.pngbin845 -> 0 bytes
-rw-r--r--app/assets/images/emoji/track_next.pngbin551 -> 0 bytes
-rw-r--r--app/assets/images/emoji/track_previous.pngbin549 -> 0 bytes
-rw-r--r--app/assets/images/emoji/trackball.pngbin892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tractor.pngbin1192 -> 0 bytes
-rw-r--r--app/assets/images/emoji/traffic_light.pngbin590 -> 0 bytes
-rw-r--r--app/assets/images/emoji/train.pngbin1031 -> 0 bytes
-rw-r--r--app/assets/images/emoji/train2.pngbin1499 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tram.pngbin1065 -> 0 bytes
-rw-r--r--app/assets/images/emoji/triangular_flag_on_post.pngbin415 -> 0 bytes
-rw-r--r--app/assets/images/emoji/triangular_ruler.pngbin369 -> 0 bytes
-rw-r--r--app/assets/images/emoji/trident.pngbin668 -> 0 bytes
-rw-r--r--app/assets/images/emoji/triumph.pngbin1529 -> 0 bytes
-rw-r--r--app/assets/images/emoji/trolleybus.pngbin1168 -> 0 bytes
-rw-r--r--app/assets/images/emoji/trophy.pngbin863 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tropical_drink.pngbin1428 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tropical_fish.pngbin1676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/truck.pngbin1366 -> 0 bytes
-rw-r--r--app/assets/images/emoji/trumpet.pngbin1281 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tulip.pngbin1065 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tumbler_glass.pngbin2312 -> 0 bytes
-rw-r--r--app/assets/images/emoji/turkey.pngbin1240 -> 0 bytes
-rw-r--r--app/assets/images/emoji/turtle.pngbin1515 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tv.pngbin776 -> 0 bytes
-rw-r--r--app/assets/images/emoji/twisted_rightwards_arrows.pngbin574 -> 0 bytes
-rw-r--r--app/assets/images/emoji/two.pngbin567 -> 0 bytes
-rw-r--r--app/assets/images/emoji/two_hearts.pngbin493 -> 0 bytes
-rw-r--r--app/assets/images/emoji/two_men_holding_hands.pngbin1347 -> 0 bytes
-rw-r--r--app/assets/images/emoji/two_women_holding_hands.pngbin1544 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u5272.pngbin411 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u5408.pngbin484 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u55b6.pngbin460 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u6307.pngbin504 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u6708.pngbin409 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u6709.pngbin434 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u6e80.pngbin564 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u7121.pngbin534 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u7533.pngbin306 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u7981.pngbin584 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u7a7a.pngbin456 -> 0 bytes
-rw-r--r--app/assets/images/emoji/umbrella.pngbin1229 -> 0 bytes
-rw-r--r--app/assets/images/emoji/umbrella2.pngbin897 -> 0 bytes
-rw-r--r--app/assets/images/emoji/unamused.pngbin632 -> 0 bytes
-rw-r--r--app/assets/images/emoji/underage.pngbin863 -> 0 bytes
-rw-r--r--app/assets/images/emoji/unicorn.pngbin2107 -> 0 bytes
-rw-r--r--app/assets/images/emoji/unlock.pngbin856 -> 0 bytes
-rw-r--r--app/assets/images/emoji/up.pngbin405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/upside_down.pngbin602 -> 0 bytes
-rw-r--r--app/assets/images/emoji/urn.pngbin742 -> 0 bytes
-rw-r--r--app/assets/images/emoji/v.pngbin1009 -> 0 bytes
-rw-r--r--app/assets/images/emoji/v_tone1.pngbin1009 -> 0 bytes
-rw-r--r--app/assets/images/emoji/v_tone2.pngbin1009 -> 0 bytes
-rw-r--r--app/assets/images/emoji/v_tone3.pngbin1009 -> 0 bytes
-rw-r--r--app/assets/images/emoji/v_tone4.pngbin1009 -> 0 bytes
-rw-r--r--app/assets/images/emoji/v_tone5.pngbin1009 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vertical_traffic_light.pngbin752 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vhs.pngbin632 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vibration_mode.pngbin683 -> 0 bytes
-rw-r--r--app/assets/images/emoji/video_camera.pngbin1611 -> 0 bytes
-rw-r--r--app/assets/images/emoji/video_game.pngbin765 -> 0 bytes
-rw-r--r--app/assets/images/emoji/violin.pngbin1156 -> 0 bytes
-rw-r--r--app/assets/images/emoji/virgo.pngbin618 -> 0 bytes
-rw-r--r--app/assets/images/emoji/volcano.pngbin1257 -> 0 bytes
-rw-r--r--app/assets/images/emoji/volleyball.pngbin1202 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vs.pngbin604 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vulcan.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone1.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone2.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone3.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone4.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone5.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/walking.pngbin1082 -> 0 bytes
-rw-r--r--app/assets/images/emoji/walking_tone1.pngbin1084 -> 0 bytes
-rw-r--r--app/assets/images/emoji/walking_tone2.pngbin1084 -> 0 bytes
-rw-r--r--app/assets/images/emoji/walking_tone3.pngbin1066 -> 0 bytes
-rw-r--r--app/assets/images/emoji/walking_tone4.pngbin1075 -> 0 bytes
-rw-r--r--app/assets/images/emoji/walking_tone5.pngbin1065 -> 0 bytes
-rw-r--r--app/assets/images/emoji/waning_crescent_moon.pngbin1213 -> 0 bytes
-rw-r--r--app/assets/images/emoji/waning_gibbous_moon.pngbin1208 -> 0 bytes
-rw-r--r--app/assets/images/emoji/warning.pngbin565 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wastebasket.pngbin2414 -> 0 bytes
-rw-r--r--app/assets/images/emoji/watch.pngbin785 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_buffalo.pngbin1536 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_polo.pngbin1755 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone1.pngbin1758 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone2.pngbin1756 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone3.pngbin1760 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone4.pngbin1749 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone5.pngbin1748 -> 0 bytes
-rw-r--r--app/assets/images/emoji/watermelon.pngbin1275 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wave.pngbin1300 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wave_tone1.pngbin1300 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wave_tone2.pngbin1300 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wave_tone3.pngbin1295 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wave_tone4.pngbin1300 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wave_tone5.pngbin1300 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wavy_dash.pngbin359 -> 0 bytes
-rw-r--r--app/assets/images/emoji/waxing_crescent_moon.pngbin1199 -> 0 bytes
-rw-r--r--app/assets/images/emoji/waxing_gibbous_moon.pngbin1229 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wc.pngbin752 -> 0 bytes
-rw-r--r--app/assets/images/emoji/weary.pngbin871 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wedding.pngbin1260 -> 0 bytes
-rw-r--r--app/assets/images/emoji/whale.pngbin1572 -> 0 bytes
-rw-r--r--app/assets/images/emoji/whale2.pngbin1196 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wheel_of_dharma.pngbin666 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wheelchair.pngbin683 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_check_mark.pngbin547 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_circle.pngbin351 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_flower.pngbin941 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_large_square.pngbin110 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_medium_small_square.pngbin110 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_medium_square.pngbin108 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_small_square.pngbin108 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_square_button.pngbin122 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_sun_cloud.pngbin968 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_sun_rain_cloud.pngbin1161 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_sun_small_cloud.pngbin989 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wilted_rose.pngbin1349 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wind_blowing_face.pngbin1827 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wind_chime.pngbin1046 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wine_glass.pngbin655 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wink.pngbin746 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wolf.pngbin1528 -> 0 bytes
-rw-r--r--app/assets/images/emoji/woman.pngbin1212 -> 0 bytes
-rw-r--r--app/assets/images/emoji/woman_tone1.pngbin1212 -> 0 bytes
-rw-r--r--app/assets/images/emoji/woman_tone2.pngbin1212 -> 0 bytes
-rw-r--r--app/assets/images/emoji/woman_tone3.pngbin1202 -> 0 bytes
-rw-r--r--app/assets/images/emoji/woman_tone4.pngbin1195 -> 0 bytes
-rw-r--r--app/assets/images/emoji/woman_tone5.pngbin1202 -> 0 bytes
-rw-r--r--app/assets/images/emoji/womans_clothes.pngbin1042 -> 0 bytes
-rw-r--r--app/assets/images/emoji/womans_hat.pngbin1553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/womens.pngbin577 -> 0 bytes
-rw-r--r--app/assets/images/emoji/worried.pngbin715 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrench.pngbin418 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrestlers.pngbin2556 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone1.pngbin2563 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone2.pngbin2553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone3.pngbin2541 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone4.pngbin2553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone5.pngbin2542 -> 0 bytes
-rw-r--r--app/assets/images/emoji/writing_hand.pngbin1001 -> 0 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone1.pngbin988 -> 0 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone2.pngbin987 -> 0 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone3.pngbin977 -> 0 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone4.pngbin973 -> 0 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone5.pngbin970 -> 0 bytes
-rw-r--r--app/assets/images/emoji/x.pngbin298 -> 0 bytes
-rw-r--r--app/assets/images/emoji/yellow_heart.pngbin435 -> 0 bytes
-rw-r--r--app/assets/images/emoji/yen.pngbin421 -> 0 bytes
-rw-r--r--app/assets/images/emoji/yin_yang.pngbin776 -> 0 bytes
-rw-r--r--app/assets/images/emoji/yum.pngbin896 -> 0 bytes
-rw-r--r--app/assets/images/emoji/zap.pngbin413 -> 0 bytes
-rw-r--r--app/assets/images/emoji/zero.pngbin560 -> 0 bytes
-rw-r--r--app/assets/images/emoji/zipper_mouth.pngbin722 -> 0 bytes
-rw-r--r--app/assets/images/emoji/zzz.pngbin540 -> 0 bytes
-rw-r--r--app/assets/images/emoji@2x.pngbin2977099 -> 3624162 bytes
-rw-r--r--app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql2
-rw-r--r--app/assets/javascripts/access_tokens/index.js2
-rw-r--r--app/assets/javascripts/admin/deploy_keys/components/table.vue54
-rw-r--r--app/assets/javascripts/admin/deploy_keys/index.js23
-rw-r--r--app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue2
-rw-r--r--app/assets/javascripts/admin/users/index.js2
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue6
-rw-r--r--app/assets/javascripts/alert_management/list.js3
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue4
-rw-r--r--app/assets/javascripts/alerts_settings/graphql.js1
-rw-r--r--app/assets/javascripts/analytics/devops_report/constants.js11
-rw-r--r--app/assets/javascripts/analytics/devops_reports/components/devops_score.vue (renamed from app/assets/javascripts/analytics/devops_report/components/devops_score.vue)0
-rw-r--r--app/assets/javascripts/analytics/devops_reports/components/devops_score_callout.vue (renamed from app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue)0
-rw-r--r--app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue (renamed from app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue)0
-rw-r--r--app/assets/javascripts/analytics/devops_reports/constants.js11
-rw-r--r--app/assets/javascripts/analytics/devops_reports/devops_score.js (renamed from app/assets/javascripts/analytics/devops_report/devops_score.js)0
-rw-r--r--app/assets/javascripts/analytics/devops_reports/devops_score_disabled_service_ping.js (renamed from app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js)0
-rw-r--r--app/assets/javascripts/analytics/shared/graphql/projects.query.graphql2
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue4
-rw-r--r--app/assets/javascripts/analytics/usage_trends/index.js2
-rw-r--r--app/assets/javascripts/api/namespaces_api.js13
-rw-r--r--app/assets/javascripts/artifacts_settings/index.js2
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue133
-rw-r--r--app/assets/javascripts/badges/components/badge_settings.vue6
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue10
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_dropdown.vue4
-rw-r--r--app/assets/javascripts/blob/components/blob_content.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue15
-rw-r--r--app/assets/javascripts/blob/components/table_contents.vue5
-rw-r--r--app/assets/javascripts/blob/pipeline_tour_success_modal.vue4
-rw-r--r--app/assets/javascripts/boards/boards_util.js4
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue36
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue160
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue82
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue33
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue88
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue53
-rw-r--r--app/assets/javascripts/boards/components/new_board_button.vue47
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue6
-rw-r--r--app/assets/javascripts/boards/graphql.js1
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/board_lists.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/board_scope.fragment.graphql6
-rw-r--r--app/assets/javascripts/boards/graphql/group_board.query.graphql9
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql10
-rw-r--r--app/assets/javascripts/boards/graphql/group_projects.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/lists_issues.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/project_board.query.graphql9
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql10
-rw-r--r--app/assets/javascripts/boards/graphql/project_milestones.query.graphql2
-rw-r--r--app/assets/javascripts/boards/index.js8
-rw-r--r--app/assets/javascripts/boards/mount_filtered_search_issue_boards.js5
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js19
-rw-r--r--app/assets/javascripts/boards/new_board.js29
-rw-r--r--app/assets/javascripts/boards/stores/actions.js95
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js5
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js18
-rw-r--r--app/assets/javascripts/boards/stores/state.js1
-rw-r--r--app/assets/javascripts/breadcrumb.js18
-rw-r--r--app/assets/javascripts/chronic_duration.js417
-rw-r--r--app/assets/javascripts/ci_lint/index.js4
-rw-r--r--app/assets/javascripts/clusters/agents/components/show.vue1
-rw-r--r--app/assets/javascripts/clusters/agents/index.js2
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue32
-rw-r--r--app/assets/javascripts/clusters_list/clusters_util.js4
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_empty_state.vue81
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_table.vue162
-rw-r--r--app/assets/javascripts/clusters_list/components/agents.vue36
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue54
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_actions.vue44
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_empty_state.vue76
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_main_view.vue73
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_view_all.vue218
-rw-r--r--app/assets/javascripts/clusters_list/components/install_agent_modal.vue52
-rw-r--r--app/assets/javascripts/clusters_list/constants.js94
-rw-r--r--app/assets/javascripts/clusters_list/graphql/cache_update.js29
-rw-r--r--app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql10
-rw-r--r--app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql4
-rw-r--r--app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql12
-rw-r--r--app/assets/javascripts/clusters_list/index.js4
-rw-r--r--app/assets/javascripts/clusters_list/load_agents.js44
-rw-r--r--app/assets/javascripts/clusters_list/load_clusters.js7
-rw-r--r--app/assets/javascripts/clusters_list/load_main_view.js50
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js16
-rw-r--r--app/assets/javascripts/clusters_list/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/clusters_list/store/mutations.js5
-rw-r--r--app/assets/javascripts/clusters_list/store/state.js7
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js10
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue6
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_alert.vue33
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_error.vue31
-rw-r--r--app/assets/javascripts/content_editor/components/editor_state_observer.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue22
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue4
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue4
-rw-r--r--app/assets/javascripts/content_editor/extensions/blockquote.js12
-rw-r--r--app/assets/javascripts/content_editor/extensions/description_list.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/details.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/emoji.js36
-rw-r--r--app/assets/javascripts/content_editor/extensions/frontmatter.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/horizontal_rule.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/html_marks.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/inline_diff.js18
-rw-r--r--app/assets/javascripts/content_editor/extensions/link.js18
-rw-r--r--app/assets/javascripts/content_editor/extensions/math_inline.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/subscript.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/superscript.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/table.js43
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_cell.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_header.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_of_contents.js21
-rw-r--r--app/assets/javascripts/content_editor/extensions/word_break.js6
-rw-r--r--app/assets/javascripts/content_editor/services/feature_flags.js3
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js16
-rw-r--r--app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js23
-rw-r--r--app/assets/javascripts/content_editor/services/upload_helpers.js10
-rw-r--r--app/assets/javascripts/contextual_sidebar.js5
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue2
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js40
-rw-r--r--app/assets/javascripts/crm/components/contacts_root.vue80
-rw-r--r--app/assets/javascripts/crm/components/organizations_root.vue71
-rw-r--r--app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql22
-rw-r--r--app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql15
-rw-r--r--app/assets/javascripts/crm/contacts_bundle.js27
-rw-r--r--app/assets/javascripts/crm/organizations_bundle.js27
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue17
-rw-r--r--app/assets/javascripts/cycle_analytics/components/metric_popover.vue61
-rw-r--r--app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue49
-rw-r--r--app/assets/javascripts/cycle_analytics/constants.js7
-rw-r--r--app/assets/javascripts/cycle_analytics/index.js49
-rw-r--r--app/assets/javascripts/cycle_analytics/store/actions.js69
-rw-r--r--app/assets/javascripts/cycle_analytics/utils.js60
-rw-r--r--app/assets/javascripts/deploy_tokens/components/revoke_button.vue2
-rw-r--r--app/assets/javascripts/design_management/graphql.js1
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue18
-rw-r--r--app/assets/javascripts/diffs/components/app.vue15
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue12
-rw-r--r--app/assets/javascripts/diffs/components/diff_comment_cell.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue11
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue31
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue15
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue2
-rw-r--r--app/assets/javascripts/diffs/index.js2
-rw-r--r--app/assets/javascripts/diffs/store/actions.js34
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js6
-rw-r--r--app/assets/javascripts/diffs/utils/diff_line.js10
-rw-r--r--app/assets/javascripts/diffs/utils/discussions.js76
-rw-r--r--app/assets/javascripts/diffs/utils/file_reviews.js2
-rw-r--r--app/assets/javascripts/dropzone_input.js41
-rw-r--r--app/assets/javascripts/editor/constants.js36
-rw-r--r--app/assets/javascripts/editor/extensions/example_source_editor_extension.js116
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_extension_base.js39
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js293
-rw-r--r--app/assets/javascripts/editor/schema/ci.json49
-rw-r--r--app/assets/javascripts/editor/source_editor_extension.js17
-rw-r--r--app/assets/javascripts/editor/source_editor_instance.js271
-rw-r--r--app/assets/javascripts/emoji/index.js1
-rw-r--r--app/assets/javascripts/environments/components/delete_environment_modal.vue4
-rw-r--r--app/assets/javascripts/environments/components/new_environment_folder.vue69
-rw-r--r--app/assets/javascripts/environments/components/new_environments_app.vue47
-rw-r--r--app/assets/javascripts/environments/components/rollback_modal_manager.vue5
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js2
-rw-r--r--app/assets/javascripts/environments/graphql/client.js25
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/cancel_auto_stop.mutation.graphql5
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/delete_environment.mutation.graphql5
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/rollback_environment.mutation.graphql5
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/stop_environment.mutation.graphql5
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql2
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql8
-rw-r--r--app/assets/javascripts/environments/graphql/queries/folder.query.graphql7
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js50
-rw-r--r--app/assets/javascripts/environments/graphql/typedefs.graphql35
-rw-r--r--app/assets/javascripts/environments/index.js69
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js6
-rw-r--r--app/assets/javascripts/environments/new_index.js38
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue5
-rw-r--r--app/assets/javascripts/experimentation/utils.js23
-rw-r--r--app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue1
-rw-r--r--app/assets/javascripts/flash.js4
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue3
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js24
-rw-r--r--app/assets/javascripts/google_cloud/components/app.vue50
-rw-r--r--app/assets/javascripts/google_cloud/components/incubation_banner.vue44
-rw-r--r--app/assets/javascripts/google_cloud/components/service_accounts.vue65
-rw-r--r--app/assets/javascripts/google_cloud/index.js11
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql4
-rw-r--r--app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql5
-rw-r--r--app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/queries/users_search.query.graphql2
-rw-r--r--app/assets/javascripts/graphql_shared/utils.js8
-rw-r--r--app/assets/javascripts/group.js4
-rw-r--r--app/assets/javascripts/group_settings/components/shared_runners_form.vue41
-rw-r--r--app/assets/javascripts/group_settings/constants.js5
-rw-r--r--app/assets/javascripts/group_settings/mount_shared_runners.js21
-rw-r--r--app/assets/javascripts/groups/components/item_caret.vue2
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue5
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue4
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/shared/commit_message_field.vue137
-rw-r--r--app/assets/javascripts/ide/constants.js2
-rw-r--r--app/assets/javascripts/import_entities/components/group_dropdown.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue33
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue14
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue310
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue84
-rw-r--r--app/assets/javascripts/import_entities/import_groups/constants.js15
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js406
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql23
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql23
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql13
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js74
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js87
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js35
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql52
-rw-r--r--app/assets/javascripts/import_entities/import_groups/index.js4
-rw-r--r--app/assets/javascripts/import_entities/import_groups/services/status_poller.js39
-rw-r--r--app/assets/javascripts/import_entities/import_groups/utils.js23
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue6
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue10
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue11
-rw-r--r--app/assets/javascripts/incidents/constants.js5
-rw-r--r--app/assets/javascripts/incidents/list.js4
-rw-r--r--app/assets/javascripts/init_confirm_danger.js38
-rw-r--r--app/assets/javascripts/integrations/constants.js8
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue10
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue53
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js16
-rw-r--r--app/assets/javascripts/invite_members/components/confetti.vue33
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue232
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue27
-rw-r--r--app/assets/javascripts/invite_members/constants.js127
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js19
-rw-r--r--app/assets/javascripts/issuable/components/issuable_by_email.vue2
-rw-r--r--app/assets/javascripts/issuable_suggestions/index.js7
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue29
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue14
-rw-r--r--app/assets/javascripts/issue_show/components/fields/type.vue17
-rw-r--r--app/assets/javascripts/issue_show/components/header_actions.vue7
-rw-r--r--app/assets/javascripts/issue_show/constants.js7
-rw-r--r--app/assets/javascripts/issue_show/incident.js56
-rw-r--r--app/assets/javascripts/issue_show/issue.js7
-rw-r--r--app/assets/javascripts/issues_list/components/issues_list_app.vue26
-rw-r--r--app/assets/javascripts/issues_list/components/new_issue_dropdown.vue7
-rw-r--r--app/assets/javascripts/issues_list/constants.js28
-rw-r--r--app/assets/javascripts/issues_list/index.js6
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues.query.graphql10
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql22
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql2
-rw-r--r--app/assets/javascripts/issues_list/queries/iteration.fragment.graphql6
-rw-r--r--app/assets/javascripts/issues_list/queries/search_projects.query.graphql1
-rw-r--r--app/assets/javascripts/issues_list/service_desk_helper.js8
-rw-r--r--app/assets/javascripts/issues_list/utils.js34
-rw-r--r--app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql2
-rw-r--r--app/assets/javascripts/jira_connect/branches/index.js7
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_button.vue24
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal.vue26
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue (renamed from app/assets/javascripts/jira_connect/subscriptions/components/groups_list.vue)0
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue85
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/app.vue112
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/groups_list_item.vue85
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue36
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue54
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/constants.js2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/index.js16
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/utils.js15
-rw-r--r--app/assets/javascripts/jira_import/index.js2
-rw-r--r--app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql2
-rw-r--r--app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql2
-rw-r--r--app/assets/javascripts/jira_import/queries/initiate_jira_import.mutation.graphql2
-rw-r--r--app/assets/javascripts/jira_import/queries/search_project_members.query.graphql2
-rw-r--r--app/assets/javascripts/jobs/components/manual_variables_form.vue229
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue8
-rw-r--r--app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js4
-rw-r--r--app/assets/javascripts/lib/graphql.js5
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js27
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue47
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js55
-rw-r--r--app/assets/javascripts/lib/utils/constants.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_format_utility.js48
-rw-r--r--app/assets/javascripts/lib/utils/file_upload.js20
-rw-r--r--app/assets/javascripts/lib/utils/rails_ujs.js38
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js2
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js6
-rw-r--r--app/assets/javascripts/logs/components/environment_logs.vue2
-rw-r--r--app/assets/javascripts/members/components/app.vue5
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget.vue285
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget_form.vue324
-rw-r--r--app/assets/javascripts/monitoring/components/charts/empty_chart.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue106
-rw-r--r--app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue2
-rw-r--r--app/assets/javascripts/monitoring/monitoring_app.js6
-rw-r--r--app/assets/javascripts/monitoring/utils.js1
-rw-r--r--app/assets/javascripts/mr_notes/index.js4
-rw-r--r--app/assets/javascripts/mr_popover/index.js7
-rw-r--r--app/assets/javascripts/nav/components/responsive_home.vue1
-rw-r--r--app/assets/javascripts/nav/components/top_nav_new_dropdown.vue1
-rw-r--r--app/assets/javascripts/network/branch_graph.js2
-rw-r--r--app/assets/javascripts/notebook/cells/output/latex.vue11
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue90
-rw-r--r--app/assets/javascripts/notes/components/multiline_comment_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue13
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue32
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue18
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js56
-rw-r--r--app/assets/javascripts/notes/stores/actions.js46
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js41
-rw-r--r--app/assets/javascripts/packages/list/components/package_search.vue57
-rw-r--r--app/assets/javascripts/packages/list/components/package_title.vue47
-rw-r--r--app/assets/javascripts/packages/list/components/packages_list_app.vue28
-rw-r--r--app/assets/javascripts/packages/list/components/tokens/package_type_token.vue26
-rw-r--r--app/assets/javascripts/packages/list/constants.js6
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_button.vue (renamed from app/assets/javascripts/registry/explorer/components/delete_button.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_image.vue (renamed from app/assets/javascripts/registry/explorer/components/delete_image.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/details_header.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/tags_loader.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/image_list.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue (renamed from app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/common.js (renamed from app/assets/javascripts/registry/explorer/constants/common.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js (renamed from app/assets/javascripts/registry/explorer/constants/details.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js (renamed from app/assets/javascripts/registry/explorer/constants/expiration_policies.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/index.js (renamed from app/assets/javascripts/registry/explorer/constants/index.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js (renamed from app/assets/javascripts/registry/explorer/constants/list.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/quick_start.js (renamed from app/assets/javascripts/registry/explorer/constants/quick_start.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js14
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql (renamed from app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql (renamed from app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql (renamed from app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql (renamed from app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql (renamed from app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql (renamed from app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js (renamed from app/assets/javascripts/registry/explorer/index.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue (renamed from app/assets/javascripts/registry/explorer/pages/details.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/index.vue (renamed from app/assets/javascripts/registry/explorer/pages/index.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue (renamed from app/assets/javascripts/registry/explorer/pages/list.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/router.js (renamed from app/assets/javascripts/registry/explorer/router.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue87
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue49
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue52
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js7
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql19
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue86
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue62
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue116
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue100
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js63
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql31
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/utils.js8
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js7
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js7
-rw-r--r--app/assets/javascripts/pages/admin/deploy_keys/index/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/dev_ops_report/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue4
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js8
-rw-r--r--app/assets/javascripts/pages/groups/crm/contacts/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/crm/organizations/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/new/components/app.vue2
-rw-r--r--app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue2
-rw-r--r--app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js12
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js4
-rw-r--r--app/assets/javascripts/pages/groups/packages/index/index.js11
-rw-r--r--app/assets/javascripts/pages/groups/registry/repositories/index.js2
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue4
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/environments/index/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue6
-rw-r--r--app/assets/javascripts/pages/projects/google_cloud/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js6
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue14
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue1
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/index/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js1
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/index/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue6
-rw-r--r--app/assets/javascripts/pages/projects/project.js8
-rw-r--r--app/assets/javascripts/pages/projects/registry/repositories/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue17
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue7
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/work_items/index.js (renamed from app/assets/javascripts/pages/projects/work_items/index/index.js)0
-rw-r--r--app/assets/javascripts/pages/shared/mount_runner_instructions.js7
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue18
-rw-r--r--app/assets/javascripts/pages/shared/wikis/constants.js5
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js11
-rw-r--r--app/assets/javascripts/pages/users/terms/index/index.js4
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue4
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue16
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue23
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue14
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue49
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue18
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue1
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue52
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue56
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue53
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue7
-rw-r--r--app/assets/javascripts/pipeline_editor/components/walkthrough_popover.vue83
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js17
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/update_app_status.mutation.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/update_pipeline_etag.mutation.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/is_new_ci_config_file.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql5
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/resolvers.js23
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js11
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue51
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue70
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue58
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue8
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql3
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines_mixin.js7
-rw-r--r--app/assets/javascripts/pipelines/pipeline_shared_client.js1
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue4
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue6
-rw-r--r--app/assets/javascripts/project_visibility.js62
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue10
-rw-r--r--app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js3
-rw-r--r--app/assets/javascripts/projects/commit/init_revert_commit_modal.js3
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js7
-rw-r--r--app/assets/javascripts/projects/components/project_delete_button.vue58
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js4
-rw-r--r--app/assets/javascripts/projects/details/upload_button.vue7
-rw-r--r--app/assets/javascripts/projects/new/components/app.vue2
-rw-r--r--app/assets/javascripts/projects/new/components/new_project_url_select.vue27
-rw-r--r--app/assets/javascripts/projects/new/index.js2
-rw-r--r--app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql3
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue12
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/index.js4
-rw-r--r--app/assets/javascripts/projects/project_delete_button.js16
-rw-r--r--app/assets/javascripts/projects/project_new.js7
-rw-r--r--app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue92
-rw-r--r--app/assets/javascripts/projects/settings/topics/index.js51
-rw-r--r--app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql9
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue7
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue60
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue115
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js2
-rw-r--r--app/assets/javascripts/projects/storage_counter/components/storage_table.vue62
-rw-r--r--app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue35
-rw-r--r--app/assets/javascripts/projects/storage_counter/constants.js12
-rw-r--r--app/assets/javascripts/projects/storage_counter/index.js2
-rw-r--r--app/assets/javascripts/projects/storage_counter/utils.js4
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue2
-rw-r--r--app/assets/javascripts/projects/upload_file.js33
-rw-r--r--app/assets/javascripts/projects/upload_file_experiment.js33
-rw-r--r--app/assets/javascripts/projects/upload_file_experiment_tracking.js9
-rw-r--r--app/assets/javascripts/ref/constants.js4
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/index.js15
-rw-r--r--app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue87
-rw-r--r--app/assets/javascripts/related_merge_requests/store/actions.js4
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue7
-rw-r--r--app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql2
-rw-r--r--app/assets/javascripts/releases/mount_index.js1
-rw-r--r--app/assets/javascripts/releases/mount_show.js7
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue19
-rw-r--r--app/assets/javascripts/repository/components/delete_blob_modal.vue13
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue19
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue4
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue27
-rw-r--r--app/assets/javascripts/repository/components/upload_blob_modal.vue4
-rw-r--r--app/assets/javascripts/repository/graphql.js1
-rw-r--r--app/assets/javascripts/repository/mixins/preload.js3
-rw-r--r--app/assets/javascripts/repository/queries/blob_info.query.graphql1
-rw-r--r--app/assets/javascripts/rest_api.js1
-rw-r--r--app/assets/javascripts/right_sidebar.js7
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue87
-rw-r--r--app/assets/javascripts/runner/admin_runners/index.js29
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_actions_cell.vue4
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_status_cell.vue40
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_summary_cell.vue27
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_type_cell.vue47
-rw-r--r--app/assets/javascripts/runner/components/helpers/masked_value.vue60
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_dropdown.vue112
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_token.vue83
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue113
-rw-r--r--app/assets/javascripts/runner/components/runner_contacted_state_badge.vue69
-rw-r--r--app/assets/javascripts/runner/components/runner_filtered_search_bar.vue27
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue39
-rw-r--r--app/assets/javascripts/runner/components/runner_manual_setup_help.vue108
-rw-r--r--app/assets/javascripts/runner/components/runner_paused_badge.vue (renamed from app/assets/javascripts/runner/components/runner_state_paused_badge.vue)0
-rw-r--r--app/assets/javascripts/runner/components/runner_registration_token_reset.vue114
-rw-r--r--app/assets/javascripts/runner/components/runner_state_locked_badge.vue25
-rw-r--r--app/assets/javascripts/runner/components/runner_tag.vue35
-rw-r--r--app/assets/javascripts/runner/components/runner_tags.vue10
-rw-r--r--app/assets/javascripts/runner/components/runner_type_alert.vue5
-rw-r--r--app/assets/javascripts/runner/components/runner_type_badge.vue5
-rw-r--r--app/assets/javascripts/runner/components/runner_type_tabs.vue66
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/status_token_config.js28
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/type_token_config.js20
-rw-r--r--app/assets/javascripts/runner/constants.js16
-rw-r--r--app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql14
-rw-r--r--app/assets/javascripts/runner/graphql/runner_node.fragment.graphql3
-rw-r--r--app/assets/javascripts/runner/graphql/runner_update.mutation.graphql3
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue24
-rw-r--r--app/assets/javascripts/runner/group_runners/index.js9
-rw-r--r--app/assets/javascripts/runner/runner_details/index.js7
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js88
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue6
-rw-r--r--app/assets/javascripts/search/sidebar/components/radio_filter.vue4
-rw-r--r--app/assets/javascripts/search/sidebar/constants/state_filter_data.js2
-rw-r--r--app/assets/javascripts/search/store/actions.js10
-rw-r--r--app/assets/javascripts/search/store/constants.js5
-rw-r--r--app/assets/javascripts/search/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/search/store/mutations.js3
-rw-r--r--app/assets/javascripts/search/store/state.js3
-rw-r--r--app/assets/javascripts/search/store/utils.js12
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue22
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js51
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue6
-rw-r--r--app/assets/javascripts/security_configuration/graphql/configure_iac.mutation.graphql6
-rw-r--r--app/assets/javascripts/security_configuration/index.js2
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue10
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue50
-rw-r--r--app/assets/javascripts/sidebar/components/attention_requested_toggle.vue74
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue17
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue8
-rw-r--r--app/assets/javascripts/sidebar/constants.js28
-rw-r--r--app/assets/javascripts/sidebar/graphql.js1
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js4
-rw-r--r--app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql7
-rw-r--r--app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql1
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js12
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js43
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js12
-rw-r--r--app/assets/javascripts/snippets/index.js1
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/index.js1
-rw-r--r--app/assets/javascripts/static_site_editor/pages/success.vue2
-rw-r--r--app/assets/javascripts/task_list.js2
-rw-r--r--app/assets/javascripts/terms/components/app.vue117
-rw-r--r--app/assets/javascripts/terms/index.js23
-rw-r--r--app/assets/javascripts/token_access/index.js2
-rw-r--r--app/assets/javascripts/user_lists/components/user_lists_table.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue77
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue74
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue41
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue254
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue32
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/issues.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js6
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue7
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/index.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/alerts_deprecation_warning.vue46
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue54
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js28
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue104
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/constants.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue97
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue65
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue85
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue60
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue65
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/title_area.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/settings/settings_block.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue106
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue49
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue64
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue82
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql15
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql10
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql14
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql8
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue184
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue4
-rw-r--r--app/assets/javascripts/vue_shared/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/directives/validation.js6
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue2
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/provider.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue10
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js1
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js1
-rw-r--r--app/assets/javascripts/work_items/components/app.vue10
-rw-r--r--app/assets/javascripts/work_items/constants.js3
-rw-r--r--app/assets/javascripts/work_items/graphql/fragmentTypes.json1
-rw-r--r--app/assets/javascripts/work_items/graphql/provider.js55
-rw-r--r--app/assets/javascripts/work_items/graphql/resolvers.js0
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql38
-rw-r--r--app/assets/javascripts/work_items/graphql/widget.fragment.graphql3
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql16
-rw-r--r--app/assets/javascripts/work_items/index.js4
-rw-r--r--app/assets/javascripts/work_items/pages/work_item_root.vue48
-rw-r--r--app/assets/javascripts/work_items/router/index.js14
-rw-r--r--app/assets/javascripts/work_items/router/routes.js8
-rw-r--r--app/assets/stylesheets/emoji_sprites.scss54
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/files.scss32
-rw-r--r--app/assets/stylesheets/framework/kbd.scss16
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss6
-rw-r--r--app/assets/stylesheets/framework/terms.scss60
-rw-r--r--app/assets/stylesheets/highlight/common.scss9
-rw-r--r--app/assets/stylesheets/mailer.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/jira_connect.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/terms.scss64
-rw-r--r--app/assets/stylesheets/pages/clusters.scss29
-rw-r--r--app/assets/stylesheets/pages/deploy_keys.scss9
-rw-r--r--app/assets/stylesheets/pages/issuable.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/startup/_cloaking.scss2
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss12
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss2
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss2
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss22
-rw-r--r--app/assets/stylesheets/themes/theme_blue.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_dark.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_green.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_indigo.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_blue.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_green.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_indigo.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_red.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_red.scss2
-rw-r--r--app/assets/stylesheets/utilities.scss16
-rw-r--r--app/controllers/admin/application_settings_controller.rb4
-rw-r--r--app/controllers/admin/applications_controller.rb3
-rw-r--r--app/controllers/admin/hook_logs_controller.rb3
-rw-r--r--app/controllers/admin/hooks_controller.rb5
-rw-r--r--app/controllers/admin/integrations_controller.rb2
-rw-r--r--app/controllers/admin/labels_controller.rb2
-rw-r--r--app/controllers/admin/runners_controller.rb10
-rw-r--r--app/controllers/application_controller.rb3
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/boards/issues_controller.rb2
-rw-r--r--app/controllers/boards/lists_controller.rb2
-rw-r--r--app/controllers/concerns/gitlab_recaptcha.rb22
-rw-r--r--app/controllers/concerns/group_tree.rb8
-rw-r--r--app/controllers/concerns/hooks_execution.rb47
-rw-r--r--app/controllers/concerns/integrations/actions.rb73
-rw-r--r--app/controllers/concerns/integrations/hooks_execution.rb47
-rw-r--r--app/controllers/concerns/integrations/params.rb4
-rw-r--r--app/controllers/concerns/integrations_actions.rb71
-rw-r--r--app/controllers/concerns/issuable_actions.rb35
-rw-r--r--app/controllers/concerns/oauth_applications.rb10
-rw-r--r--app/controllers/concerns/one_trust_csp.rb2
-rw-r--r--app/controllers/concerns/workhorse_authorization.rb6
-rw-r--r--app/controllers/confirmations_controller.rb10
-rw-r--r--app/controllers/dashboard/labels_controller.rb2
-rw-r--r--app/controllers/dashboard/milestones_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/dashboard_controller.rb2
-rw-r--r--app/controllers/explore/projects_controller.rb22
-rw-r--r--app/controllers/graphql_controller.rb1
-rw-r--r--app/controllers/groups/autocomplete_sources_controller.rb2
-rw-r--r--app/controllers/groups/boards_controller.rb6
-rw-r--r--app/controllers/groups/crm_controller.rb30
-rw-r--r--app/controllers/groups/dependency_proxy_for_containers_controller.rb88
-rw-r--r--app/controllers/groups/email_campaigns_controller.rb7
-rw-r--r--app/controllers/groups/labels_controller.rb2
-rw-r--r--app/controllers/groups/milestones_controller.rb2
-rw-r--r--app/controllers/groups/packages_controller.rb4
-rw-r--r--app/controllers/groups/settings/applications_controller.rb3
-rw-r--r--app/controllers/groups/settings/integrations_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb10
-rw-r--r--app/controllers/help_controller.rb2
-rw-r--r--app/controllers/import/bitbucket_controller.rb4
-rw-r--r--app/controllers/invites_controller.rb2
-rw-r--r--app/controllers/jira_connect/app_descriptor_controller.rb2
-rw-r--r--app/controllers/jira_connect/application_controller.rb4
-rw-r--r--app/controllers/jira_connect/events_controller.rb7
-rw-r--r--app/controllers/jira_connect/subscriptions_controller.rb4
-rw-r--r--app/controllers/jwks_controller.rb10
-rw-r--r--app/controllers/oauth/applications_controller.rb6
-rw-r--r--app/controllers/passwords_controller.rb10
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb4
-rw-r--r--app/controllers/profiles_controller.rb2
-rw-r--r--app/controllers/projects/alert_management_controller.rb4
-rw-r--r--app/controllers/projects/alerting/notifications_controller.rb6
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb2
-rw-r--r--app/controllers/projects/badges_controller.rb3
-rw-r--r--app/controllers/projects/blame_controller.rb1
-rw-r--r--app/controllers/projects/blob_controller.rb1
-rw-r--r--app/controllers/projects/boards_controller.rb6
-rw-r--r--app/controllers/projects/branches_controller.rb4
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb9
-rw-r--r--app/controllers/projects/cluster_agents_controller.rb6
-rw-r--r--app/controllers/projects/commit_controller.rb1
-rw-r--r--app/controllers/projects/commits_controller.rb7
-rw-r--r--app/controllers/projects/compare_controller.rb1
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb12
-rw-r--r--app/controllers/projects/discussions_controller.rb2
-rw-r--r--app/controllers/projects/environments_controller.rb4
-rw-r--r--app/controllers/projects/find_file_controller.rb1
-rw-r--r--app/controllers/projects/forks_controller.rb1
-rw-r--r--app/controllers/projects/google_cloud_controller.rb26
-rw-r--r--app/controllers/projects/graphs_controller.rb5
-rw-r--r--app/controllers/projects/hook_logs_controller.rb3
-rw-r--r--app/controllers/projects/hooks_controller.rb5
-rw-r--r--app/controllers/projects/issue_links_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb7
-rw-r--r--app/controllers/projects/jobs_controller.rb8
-rw-r--r--app/controllers/projects/labels_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb6
-rw-r--r--app/controllers/projects/metrics_dashboard_controller.rb1
-rw-r--r--app/controllers/projects/milestones_controller.rb2
-rw-r--r--app/controllers/projects/network_controller.rb1
-rw-r--r--app/controllers/projects/notes_controller.rb10
-rw-r--r--app/controllers/projects/packages/packages_controller.rb4
-rw-r--r--app/controllers/projects/prometheus/alerts_controller.rb6
-rw-r--r--app/controllers/projects/refs_controller.rb1
-rw-r--r--app/controllers/projects/releases/evidences_controller.rb1
-rw-r--r--app/controllers/projects/releases_controller.rb3
-rw-r--r--app/controllers/projects/services_controller.rb1
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb3
-rw-r--r--app/controllers/projects/settings/repository_controller.rb1
-rw-r--r--app/controllers/projects/tags_controller.rb26
-rw-r--r--app/controllers/projects/templates_controller.rb1
-rw-r--r--app/controllers/projects/todos_controller.rb2
-rw-r--r--app/controllers/projects/tree_controller.rb3
-rw-r--r--app/controllers/projects/usage_quotas_controller.rb7
-rw-r--r--app/controllers/projects/wikis_controller.rb6
-rw-r--r--app/controllers/projects_controller.rb16
-rw-r--r--app/controllers/registrations/welcome_controller.rb17
-rw-r--r--app/controllers/repositories/lfs_api_controller.rb1
-rw-r--r--app/controllers/repositories/lfs_storage_controller.rb2
-rw-r--r--app/controllers/search_controller.rb3
-rw-r--r--app/experiments/change_continuous_onboarding_link_urls_experiment.rb9
-rw-r--r--app/experiments/templates/new_project_readme_content/readme_advanced.md.tt4
-rw-r--r--app/finders/autocomplete/routes_finder.rb2
-rw-r--r--app/finders/award_emojis_finder.rb2
-rw-r--r--app/finders/ci/pipelines_finder.rb17
-rw-r--r--app/finders/ci/pipelines_for_merge_request_finder.rb66
-rw-r--r--app/finders/clusters/agent_authorizations_finder.rb65
-rw-r--r--app/finders/environments/environments_by_deployments_finder.rb16
-rw-r--r--app/finders/events_finder.rb5
-rw-r--r--app/finders/issuables/label_filter.rb8
-rw-r--r--app/finders/members_finder.rb15
-rw-r--r--app/finders/packages/group_packages_finder.rb2
-rw-r--r--app/finders/packages/package_finder.rb2
-rw-r--r--app/finders/packages/packages_finder.rb3
-rw-r--r--app/finders/security/security_jobs_finder.rb2
-rw-r--r--app/finders/snippets_finder.rb27
-rw-r--r--app/finders/tags_finder.rb35
-rw-r--r--app/graphql/gitlab_schema.rb8
-rw-r--r--app/graphql/mutations/customer_relations/contacts/create.rb4
-rw-r--r--app/graphql/mutations/customer_relations/contacts/update.rb4
-rw-r--r--app/graphql/mutations/customer_relations/organizations/create.rb4
-rw-r--r--app/graphql/mutations/customer_relations/organizations/update.rb4
-rw-r--r--app/graphql/mutations/issues/create.rb9
-rw-r--r--app/graphql/mutations/issues/set_crm_contacts.rb48
-rw-r--r--app/graphql/mutations/merge_requests/set_wip.rb35
-rw-r--r--app/graphql/mutations/merge_requests/toggle_attention_requested.rb27
-rw-r--r--app/graphql/mutations/security/ci_configuration/configure_sast_iac.rb21
-rw-r--r--app/graphql/queries/epic/epic_children.query.graphql1
-rw-r--r--app/graphql/resolvers/base_issues_resolver.rb61
-rw-r--r--app/graphql/resolvers/ci/pipeline_job_artifacts_resolver.rb25
-rw-r--r--app/graphql/resolvers/concerns/issue_resolver_arguments.rb26
-rw-r--r--app/graphql/resolvers/concerns/resolves_groups.rb34
-rw-r--r--app/graphql/resolvers/concerns/resolves_pipelines.rb3
-rw-r--r--app/graphql/resolvers/group_issues_resolver.rb12
-rw-r--r--app/graphql/resolvers/groups_resolver.rb12
-rw-r--r--app/graphql/resolvers/issue_status_counts_resolver.rb1
-rw-r--r--app/graphql/resolvers/issues_resolver.rb59
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb6
-rw-r--r--app/graphql/resolvers/topics_resolver.rb19
-rw-r--r--app/graphql/resolvers/users/groups_resolver.rb17
-rw-r--r--app/graphql/types/ci/job_artifact_type.rb4
-rw-r--r--app/graphql/types/ci/pipeline_scope_enum.rb21
-rw-r--r--app/graphql/types/ci/pipeline_status_enum.rb22
-rw-r--r--app/graphql/types/ci/pipeline_type.rb9
-rw-r--r--app/graphql/types/ci/runner_setup_type.rb2
-rw-r--r--app/graphql/types/commit_type.rb7
-rw-r--r--app/graphql/types/customer_relations/contact_type.rb2
-rw-r--r--app/graphql/types/customer_relations/organization_type.rb2
-rw-r--r--app/graphql/types/dependency_proxy/manifest_type.rb1
-rw-r--r--app/graphql/types/evidence_type.rb2
-rw-r--r--app/graphql/types/group_type.rb10
-rw-r--r--app/graphql/types/issues/negated_issue_filter_input_type.rb3
-rw-r--r--app/graphql/types/merge_request_type.rb6
-rw-r--r--app/graphql/types/mutation_type.rb6
-rw-r--r--app/graphql/types/packages/file_metadata_type.rb5
-rw-r--r--app/graphql/types/packages/helm/dependency_type.rb28
-rw-r--r--app/graphql/types/packages/helm/file_metadatum_type.rb19
-rw-r--r--app/graphql/types/packages/helm/maintainer_type.rb18
-rw-r--r--app/graphql/types/packages/helm/metadata_type.rb32
-rw-r--r--app/graphql/types/packages/package_file_type.rb2
-rw-r--r--app/graphql/types/project_type.rb7
-rw-r--r--app/graphql/types/projects/topic_type.rb28
-rw-r--r--app/graphql/types/query_type.rb5
-rw-r--r--app/graphql/types/release_links_type.rb17
-rw-r--r--app/graphql/types/release_tag_wildcard_id_enum.rb11
-rw-r--r--app/graphql/types/release_type.rb3
-rw-r--r--app/graphql/types/repository/blob_type.rb3
-rw-r--r--app/graphql/types/repository_type.rb3
-rw-r--r--app/helpers/admin/deploy_key_helper.rb14
-rw-r--r--app/helpers/appearances_helper.rb4
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/auth_helper.rb1
-rw-r--r--app/helpers/boards_helper.rb2
-rw-r--r--app/helpers/breadcrumbs_helper.rb8
-rw-r--r--app/helpers/ci/pipelines_helper.rb11
-rw-r--r--app/helpers/ci/runners_helper.rb40
-rw-r--r--app/helpers/clusters_helper.rb37
-rw-r--r--app/helpers/emoji_helper.rb5
-rw-r--r--app/helpers/environments_helper.rb8
-rw-r--r--app/helpers/graph_helper.rb4
-rw-r--r--app/helpers/groups/settings_helper.rb20
-rw-r--r--app/helpers/groups_helper.rb4
-rw-r--r--app/helpers/icons_helper.rb4
-rw-r--r--app/helpers/integrations_helper.rb14
-rw-r--r--app/helpers/invite_members_helper.rb23
-rw-r--r--app/helpers/issuables_description_templates_helper.rb13
-rw-r--r--app/helpers/issuables_helper.rb9
-rw-r--r--app/helpers/issues_helper.rb4
-rw-r--r--app/helpers/learn_gitlab_helper.rb44
-rw-r--r--app/helpers/members_helper.rb8
-rw-r--r--app/helpers/nav/top_nav_helper.rb1
-rw-r--r--app/helpers/notes_helper.rb9
-rw-r--r--app/helpers/one_trust_helper.rb3
-rw-r--r--app/helpers/projects/alert_management_helper.rb5
-rw-r--r--app/helpers/projects/incidents_helper.rb3
-rw-r--r--app/helpers/projects/security/configuration_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb36
-rw-r--r--app/helpers/recaptcha_helper.rb3
-rw-r--r--app/helpers/reminder_emails_helper.rb3
-rw-r--r--app/helpers/routing/pseudonymization_helper.rb100
-rw-r--r--app/helpers/storage_helper.rb3
-rw-r--r--app/helpers/tab_helper.rb12
-rw-r--r--app/helpers/terms_helper.rb20
-rw-r--r--app/helpers/time_zone_helper.rb8
-rw-r--r--app/helpers/todos_helper.rb1
-rw-r--r--app/helpers/user_callouts_helper.rb21
-rw-r--r--app/helpers/wiki_helper.rb6
-rw-r--r--app/helpers/workhorse_helper.rb4
-rw-r--r--app/mailers/emails/members.rb6
-rw-r--r--app/mailers/emails/pipelines.rb21
-rw-r--r--app/models/analytics/cycle_analytics/issue_stage_event.rb13
-rw-r--r--app/models/analytics/cycle_analytics/merge_request_stage_event.rb13
-rw-r--r--app/models/application_record.rb8
-rw-r--r--app/models/application_setting.rb12
-rw-r--r--app/models/application_setting_implementation.rb7
-rw-r--r--app/models/audit_event.rb6
-rw-r--r--app/models/award_emoji.rb2
-rw-r--r--app/models/blob_viewer/package_json.rb14
-rw-r--r--app/models/bulk_imports/entity.rb4
-rw-r--r--app/models/chat_name.rb4
-rw-r--r--app/models/ci/application_record.rb5
-rw-r--r--app/models/ci/build.rb58
-rw-r--r--app/models/ci/build_metadata.rb12
-rw-r--r--app/models/ci/build_need.rb8
-rw-r--r--app/models/ci/build_runner_session.rb2
-rw-r--r--app/models/ci/build_trace_chunk.rb1
-rw-r--r--app/models/ci/ci_database_record.rb17
-rw-r--r--app/models/ci/instance_variable.rb2
-rw-r--r--app/models/ci/job_artifact.rb22
-rw-r--r--app/models/ci/pipeline.rb110
-rw-r--r--app/models/ci/runner.rb34
-rw-r--r--app/models/ci/sources/pipeline.rb3
-rw-r--r--app/models/ci/stage.rb1
-rw-r--r--app/models/ci/trigger.rb4
-rw-r--r--app/models/clusters/agents/group_authorization.rb2
-rw-r--r--app/models/clusters/agents/project_authorization.rb2
-rw-r--r--app/models/clusters/applications/runner.rb4
-rw-r--r--app/models/clusters/cluster.rb8
-rw-r--r--app/models/clusters/integrations/prometheus.rb7
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/alert_event_lifecycle.rb2
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage_event_model.rb28
-rw-r--r--app/models/concerns/cascading_namespace_setting_attribute.rb6
-rw-r--r--app/models/concerns/ci/contextable.rb33
-rw-r--r--app/models/concerns/ci/has_status.rb17
-rw-r--r--app/models/concerns/ci/metadatable.rb3
-rw-r--r--app/models/concerns/clusters/agents/authorization_config_scopes.rb25
-rw-r--r--app/models/concerns/database_reflection.rb21
-rw-r--r--app/models/concerns/enums/vulnerability.rb13
-rw-r--r--app/models/concerns/file_store_mounter.rb8
-rw-r--r--app/models/concerns/has_integrations.rb19
-rw-r--r--app/models/concerns/has_user_type.rb1
-rw-r--r--app/models/concerns/integrations/push_data_validations.rb44
-rw-r--r--app/models/concerns/integrations/reactively_cached.rb15
-rw-r--r--app/models/concerns/issuable.rb1
-rw-r--r--app/models/concerns/legacy_bulk_insert.rb54
-rw-r--r--app/models/concerns/loaded_in_group_list.rb2
-rw-r--r--app/models/concerns/loose_foreign_key.rb22
-rw-r--r--app/models/concerns/merge_request_reviewer_state.rb25
-rw-r--r--app/models/concerns/milestoneable.rb3
-rw-r--r--app/models/concerns/noteable.rb33
-rw-r--r--app/models/concerns/reactive_service.rb13
-rw-r--r--app/models/concerns/security/latest_pipeline_information.rb36
-rw-r--r--app/models/concerns/service_push_data_validations.rb43
-rw-r--r--app/models/concerns/sha256_attribute.rb2
-rw-r--r--app/models/concerns/sha_attribute.rb2
-rw-r--r--app/models/concerns/strip_attribute.rb3
-rw-r--r--app/models/concerns/timebox.rb16
-rw-r--r--app/models/concerns/transactions.rb28
-rw-r--r--app/models/concerns/ttl_expirable.rb7
-rw-r--r--app/models/concerns/update_highest_role.rb2
-rw-r--r--app/models/concerns/x509_serial_number_attribute.rb2
-rw-r--r--app/models/custom_emoji.rb2
-rw-r--r--app/models/customer_relations/contact.rb3
-rw-r--r--app/models/customer_relations/issue_contact.rb20
-rw-r--r--app/models/data_list.rb14
-rw-r--r--app/models/dependency_proxy/blob.rb2
-rw-r--r--app/models/dependency_proxy/manifest.rb12
-rw-r--r--app/models/deploy_key.rb8
-rw-r--r--app/models/deployment.rb20
-rw-r--r--app/models/design_management/version.rb2
-rw-r--r--app/models/email.rb12
-rw-r--r--app/models/environment.rb8
-rw-r--r--app/models/error_tracking/error.rb7
-rw-r--r--app/models/error_tracking/error_event.rb8
-rw-r--r--app/models/event.rb3
-rw-r--r--app/models/gpg_signature.rb3
-rw-r--r--app/models/group.rb30
-rw-r--r--app/models/integration.rb4
-rw-r--r--app/models/integrations/bamboo.rb2
-rw-r--r--app/models/integrations/base_chat_notification.rb7
-rw-r--r--app/models/integrations/buildkite.rb2
-rw-r--r--app/models/integrations/discord.rb7
-rw-r--r--app/models/integrations/drone_ci.rb4
-rw-r--r--app/models/integrations/emails_on_push.rb9
-rw-r--r--app/models/integrations/hangouts_chat.rb7
-rw-r--r--app/models/integrations/jira.rb15
-rw-r--r--app/models/integrations/microsoft_teams.rb7
-rw-r--r--app/models/integrations/pipelines_email.rb18
-rw-r--r--app/models/integrations/shimo.rb47
-rw-r--r--app/models/integrations/teamcity.rb4
-rw-r--r--app/models/integrations/unify_circuit.rb7
-rw-r--r--app/models/integrations/webex_teams.rb7
-rw-r--r--app/models/integrations/zentao.rb34
-rw-r--r--app/models/issue.rb10
-rw-r--r--app/models/key.rb2
-rw-r--r--app/models/label_link.rb12
-rw-r--r--app/models/loose_foreign_keys/deleted_record.rb29
-rw-r--r--app/models/loose_foreign_keys/modification_tracker.rb51
-rw-r--r--app/models/member.rb11
-rw-r--r--app/models/members/member_task.rb43
-rw-r--r--app/models/members/project_member.rb1
-rw-r--r--app/models/merge_request.rb27
-rw-r--r--app/models/merge_request_assignee.rb6
-rw-r--r--app/models/merge_request_context_commit.rb2
-rw-r--r--app/models/merge_request_context_commit_diff_file.rb2
-rw-r--r--app/models/merge_request_diff.rb8
-rw-r--r--app/models/merge_request_diff_commit.rb25
-rw-r--r--app/models/merge_request_reviewer.rb13
-rw-r--r--app/models/namespace.rb59
-rw-r--r--app/models/namespaces/project_namespace.rb2
-rw-r--r--app/models/namespaces/traversal/linear.rb3
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb78
-rw-r--r--app/models/namespaces/traversal/recursive_scopes.rb7
-rw-r--r--app/models/namespaces/user_namespace.rb20
-rw-r--r--app/models/note.rb1
-rw-r--r--app/models/packages/npm.rb4
-rw-r--r--app/models/packages/npm/metadatum.rb25
-rw-r--r--app/models/packages/package.rb8
-rw-r--r--app/models/packages/package_file.rb3
-rw-r--r--app/models/preloaders/group_policy_preloader.rb23
-rw-r--r--app/models/preloaders/group_root_ancestor_preloader.rb32
-rw-r--r--app/models/preloaders/user_max_access_level_in_groups_preloader.rb29
-rw-r--r--app/models/project.rb80
-rw-r--r--app/models/project_authorization.rb5
-rw-r--r--app/models/project_setting.rb2
-rw-r--r--app/models/project_team.rb6
-rw-r--r--app/models/projects/topic.rb2
-rw-r--r--app/models/push_event_payload.rb3
-rw-r--r--app/models/release.rb1
-rw-r--r--app/models/repository.rb41
-rw-r--r--app/models/suggestion.rb1
-rw-r--r--app/models/todo.rb8
-rw-r--r--app/models/upload.rb11
-rw-r--r--app/models/uploads/fog.rb14
-rw-r--r--app/models/uploads/local.rb2
-rw-r--r--app/models/user.rb103
-rw-r--r--app/models/user_status.rb4
-rw-r--r--app/models/users/credit_card_validation.rb3
-rw-r--r--app/models/users/in_product_marketing_email.rb3
-rw-r--r--app/models/users_statistics.rb36
-rw-r--r--app/models/webauthn_registration.rb3
-rw-r--r--app/policies/group_policy.rb17
-rw-r--r--app/policies/issue_policy.rb7
-rw-r--r--app/policies/packages/helm/file_metadatum_policy.rb8
-rw-r--r--app/policies/project_policy.rb15
-rw-r--r--app/presenters/award_emoji_presenter.rb10
-rw-r--r--app/presenters/blob_presenter.rb33
-rw-r--r--app/presenters/ci/build_runner_presenter.rb6
-rw-r--r--app/presenters/packages/npm/package_presenter.rb38
-rw-r--r--app/presenters/project_presenter.rb38
-rw-r--r--app/presenters/release_presenter.rb4
-rw-r--r--app/serializers/alert_management/alert_entity.rb8
-rw-r--r--app/serializers/alert_management/alert_serializer.rb7
-rw-r--r--app/serializers/analytics_summary_entity.rb1
-rw-r--r--app/serializers/merge_request_sidebar_extras_entity.rb4
-rw-r--r--app/serializers/merge_request_user_entity.rb18
-rw-r--r--app/serializers/merge_request_widget_entity.rb17
-rw-r--r--app/serializers/note_entity.rb4
-rw-r--r--app/serializers/service_field_entity.rb2
-rw-r--r--app/services/alert_management/process_prometheus_alert_service.rb7
-rw-r--r--app/services/audit_event_service.rb5
-rw-r--r--app/services/authorized_project_update/project_access_changed_service.rb19
-rw-r--r--app/services/award_emojis/base_service.rb2
-rw-r--r--app/services/base_service.rb8
-rw-r--r--app/services/bulk_imports/file_download_service.rb12
-rw-r--r--app/services/bulk_update_integration_service.rb7
-rw-r--r--app/services/ci/create_pipeline_service.rb1
-rw-r--r--app/services/ci/destroy_pipeline_service.rb4
-rw-r--r--app/services/ci/external_pull_requests/create_pipeline_service.rb11
-rw-r--r--app/services/ci/generate_kubeconfig_service.rb62
-rw-r--r--app/services/ci/job_artifacts/create_service.rb41
-rw-r--r--app/services/ci/job_artifacts/destroy_all_expired_service.rb18
-rw-r--r--app/services/ci/job_artifacts/destroy_batch_service.rb23
-rw-r--r--app/services/ci/parse_dotenv_artifact_service.rb19
-rw-r--r--app/services/ci/retry_build_service.rb7
-rw-r--r--app/services/ci/unlock_artifacts_service.rb100
-rw-r--r--app/services/ci/update_build_state_service.rb4
-rw-r--r--app/services/clusters/agents/refresh_authorization_service.rb2
-rw-r--r--app/services/clusters/applications/prometheus_health_check_service.rb87
-rw-r--r--app/services/clusters/cleanup/project_namespace_service.rb6
-rw-r--r--app/services/clusters/cleanup/service_account_service.rb5
-rw-r--r--app/services/clusters/integrations/prometheus_health_check_service.rb101
-rw-r--r--app/services/concerns/alert_management/responses.rb26
-rw-r--r--app/services/concerns/issues/issue_type_helpers.rb12
-rw-r--r--app/services/concerns/members/bulk_create_users.rb6
-rw-r--r--app/services/customer_relations/contacts/base_service.rb2
-rw-r--r--app/services/customer_relations/organizations/base_service.rb2
-rw-r--r--app/services/dependency_proxy/find_or_create_blob_service.rb3
-rw-r--r--app/services/dependency_proxy/find_or_create_manifest_service.rb30
-rw-r--r--app/services/dependency_proxy/head_manifest_service.rb5
-rw-r--r--app/services/dependency_proxy/pull_manifest_service.rb8
-rw-r--r--app/services/deployments/archive_in_project_service.rb27
-rw-r--r--app/services/deployments/link_merge_requests_service.rb2
-rw-r--r--app/services/design_management/copy_design_collection/copy_service.rb12
-rw-r--r--app/services/emails/destroy_service.rb2
-rw-r--r--app/services/error_tracking/collect_error_service.rb15
-rw-r--r--app/services/google_cloud/service_accounts_service.rb40
-rw-r--r--app/services/groups/create_service.rb12
-rw-r--r--app/services/groups/import_export/import_service.rb2
-rw-r--r--app/services/groups/transfer_service.rb19
-rw-r--r--app/services/import/github/notes/create_service.rb15
-rw-r--r--app/services/issuable/clone/attributes_rewriter.rb2
-rw-r--r--app/services/issuable_links/list_service.rb7
-rw-r--r--app/services/issues/base_service.rb9
-rw-r--r--app/services/issues/build_service.rb2
-rw-r--r--app/services/issues/close_service.rb9
-rw-r--r--app/services/issues/create_service.rb6
-rw-r--r--app/services/issues/set_crm_contacts_service.rb90
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/jira/requests/base.rb20
-rw-r--r--app/services/labels/transfer_service.rb35
-rw-r--r--app/services/loose_foreign_keys/batch_cleaner_service.rb61
-rw-r--r--app/services/loose_foreign_keys/cleaner_service.rb99
-rw-r--r--app/services/loose_foreign_keys/process_deleted_records_service.rb74
-rw-r--r--app/services/members/create_service.rb19
-rw-r--r--app/services/members/creator_service.rb18
-rw-r--r--app/services/members/invite_service.rb5
-rw-r--r--app/services/merge_requests/outdated_discussion_diff_lines_service.rb61
-rw-r--r--app/services/merge_requests/retarget_chain_service.rb2
-rw-r--r--app/services/merge_requests/toggle_attention_requested_service.rb49
-rw-r--r--app/services/namespaces/in_product_marketing_email_records.rb26
-rw-r--r--app/services/namespaces/in_product_marketing_emails_service.rb31
-rw-r--r--app/services/namespaces/invite_team_email_service.rb62
-rw-r--r--app/services/notes/create_service.rb6
-rw-r--r--app/services/notification_service.rb4
-rw-r--r--app/services/packages/create_dependency_service.rb10
-rw-r--r--app/services/packages/npm/create_package_service.rb10
-rw-r--r--app/services/packages/nuget/create_dependency_service.rb2
-rw-r--r--app/services/packages/rubygems/create_dependencies_service.rb2
-rw-r--r--app/services/packages/update_tags_service.rb2
-rw-r--r--app/services/projects/alerting/notify_service.rb15
-rw-r--r--app/services/projects/all_issues_count_service.rb15
-rw-r--r--app/services/projects/all_merge_requests_count_service.rb15
-rw-r--r--app/services/projects/container_repository/cache_tags_created_at_service.rb70
-rw-r--r--app/services/projects/container_repository/cleanup_tags_service.rb5
-rw-r--r--app/services/projects/create_service.rb10
-rw-r--r--app/services/projects/destroy_service.rb9
-rw-r--r--app/services/projects/detect_repository_languages_service.rb2
-rw-r--r--app/services/projects/import_export/export_service.rb11
-rw-r--r--app/services/projects/lfs_pointers/lfs_link_service.rb2
-rw-r--r--app/services/projects/participants_service.rb10
-rw-r--r--app/services/projects/prometheus/alerts/notify_service.rb19
-rw-r--r--app/services/resource_events/base_synthetic_notes_builder_service.rb12
-rw-r--r--app/services/resource_events/change_labels_service.rb2
-rw-r--r--app/services/resource_events/synthetic_label_notes_builder_service.rb4
-rw-r--r--app/services/resource_events/synthetic_milestone_notes_builder_service.rb4
-rw-r--r--app/services/resource_events/synthetic_state_notes_builder_service.rb4
-rw-r--r--app/services/search_service.rb4
-rw-r--r--app/services/security/ci_configuration/sast_iac_create_service.rb25
-rw-r--r--app/services/snippets/update_service.rb2
-rw-r--r--app/services/spam/spam_verdict_service.rb12
-rw-r--r--app/services/suggestions/create_service.rb2
-rw-r--r--app/services/system_note_service.rb4
-rw-r--r--app/services/system_notes/incident_service.rb6
-rw-r--r--app/services/system_notes/issuables_service.rb8
-rw-r--r--app/services/tasks_to_be_done/base_service.rb55
-rw-r--r--app/services/tasks_to_be_done/create_ci_task_service.rb44
-rw-r--r--app/services/tasks_to_be_done/create_code_task_service.rb52
-rw-r--r--app/services/tasks_to_be_done/create_issues_task_service.rb43
-rw-r--r--app/services/todo_service.rb5
-rw-r--r--app/services/users/destroy_service.rb5
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb6
-rw-r--r--app/services/users/upsert_credit_card_validation_service.rb1
-rw-r--r--app/uploaders/bulk_imports/export_uploader.rb2
-rw-r--r--app/uploaders/file_uploader.rb9
-rw-r--r--app/uploaders/import_export_uploader.rb4
-rw-r--r--app/validators/gitlab/emoji_name_validator.rb19
-rw-r--r--app/validators/json_schemas/error_tracking_event_payload.json13
-rw-r--r--app/validators/json_schemas/helm_metadata.json18
-rw-r--r--app/validators/json_schemas/npm_package_json.json26
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml2
-rw-r--r--app/views/admin/application_settings/_protected_paths.html.haml20
-rw-r--r--app/views/admin/application_settings/_sentry.html.haml22
-rw-r--r--app/views/admin/application_settings/_usage.html.haml2
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml8
-rw-r--r--app/views/admin/application_settings/general.html.haml2
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml14
-rw-r--r--app/views/admin/application_settings/network.html.haml8
-rw-r--r--app/views/admin/application_settings/reporting.html.haml3
-rw-r--r--app/views/admin/applications/show.html.haml1
-rw-r--r--app/views/admin/dashboard/index.html.haml4
-rw-r--r--app/views/admin/dashboard/stats.html.haml63
-rw-r--r--app/views/admin/deploy_keys/index.html.haml65
-rw-r--r--app/views/admin/dev_ops_report/_score.html.haml (renamed from app/views/admin/dev_ops_report/_report.html.haml)0
-rw-r--r--app/views/admin/dev_ops_report/show.html.haml4
-rw-r--r--app/views/admin/runners/index.html.haml2
-rw-r--r--app/views/admin/topics/_topic.html.haml2
-rw-r--r--app/views/admin/users/_tabs.html.haml10
-rw-r--r--app/views/admin/users/_users.html.haml72
-rw-r--r--app/views/clusters/clusters/_cluster_list.html.haml8
-rw-r--r--app/views/clusters/clusters/_empty_state.html.haml14
-rw-r--r--app/views/clusters/clusters/aws/_new.html.haml2
-rw-r--r--app/views/clusters/clusters/gcp/_form.html.haml4
-rw-r--r--app/views/clusters/clusters/index.html.haml20
-rw-r--r--app/views/clusters/clusters/new.html.haml38
-rw-r--r--app/views/clusters/clusters/show.html.haml2
-rw-r--r--app/views/dashboard/_projects_head.html.haml14
-rw-r--r--app/views/dashboard/_projects_nav.html.haml13
-rw-r--r--app/views/dashboard/_snippets_head.html.haml10
-rw-r--r--app/views/dashboard/projects/_nav.html.haml8
-rw-r--r--app/views/dashboard/todos/index.html.haml20
-rw-r--r--app/views/devise/confirmations/new.html.haml7
-rw-r--r--app/views/devise/passwords/new.html.haml7
-rw-r--r--app/views/doorkeeper/applications/show.html.haml5
-rw-r--r--app/views/errors/omniauth_error.html.haml2
-rw-r--r--app/views/explore/groups/_nav.html.haml6
-rw-r--r--app/views/explore/projects/_nav.html.haml14
-rw-r--r--app/views/explore/projects/topic.html.haml31
-rw-r--r--app/views/explore/projects/topics.html.haml12
-rw-r--r--app/views/explore/topics/_head.html.haml9
-rw-r--r--app/views/groups/_invite_members_side_nav_link.html.haml8
-rw-r--r--app/views/groups/boards/show.html.haml2
-rw-r--r--app/views/groups/crm/contacts.html.haml4
-rw-r--r--app/views/groups/crm/organizations.html.haml4
-rw-r--r--app/views/groups/dependency_proxies/show.html.haml4
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/settings/_advanced.html.haml5
-rw-r--r--app/views/groups/settings/_membership.html.haml1
-rw-r--r--app/views/groups/settings/_permanent_deletion.html.haml6
-rw-r--r--app/views/groups/settings/_remove.html.haml7
-rw-r--r--app/views/groups/settings/_remove_button.html.haml4
-rw-r--r--app/views/groups/settings/_transfer.html.haml2
-rw-r--r--app/views/groups/settings/applications/show.html.haml5
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/help/index.html.haml4
-rw-r--r--app/views/import/github/new.html.haml2
-rw-r--r--app/views/issues/_issues_calendar.ics.ruby3
-rw-r--r--app/views/jira_connect/branches/new.html.haml1
-rw-r--r--app/views/jira_connect/subscriptions/index.html.haml15
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/_loading_hints.html.haml6
-rw-r--r--app/views/layouts/_published_experiments.html.haml4
-rw-r--r--app/views/layouts/_snowplow.html.haml3
-rw-r--r--app/views/layouts/application.html.haml1
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml2
-rw-r--r--app/views/layouts/header/_logo_with_title.html.haml4
-rw-r--r--app/views/layouts/nav/_breadcrumbs.html.haml4
-rw-r--r--app/views/layouts/nav/_top_nav_responsive.html.haml3
-rw-r--r--app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml11
-rw-r--r--app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml11
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml4
-rw-r--r--app/views/layouts/terms.html.haml9
-rw-r--r--app/views/notify/in_product_marketing_email.html.haml15
-rw-r--r--app/views/notify/in_product_marketing_email.text.erb4
-rw-r--r--app/views/notify/member_access_denied_email.html.haml9
-rw-r--r--app/views/notify/member_access_granted_email.html.haml6
-rw-r--r--app/views/notify/member_invited_email.html.haml5
-rw-r--r--app/views/profiles/_event_table.html.haml11
-rw-r--r--app/views/profiles/audit_log.html.haml2
-rw-r--r--app/views/profiles/emails/index.html.haml2
-rw-r--r--app/views/profiles/show.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml4
-rw-r--r--app/views/projects/_invite_members_side_nav_link.html.haml8
-rw-r--r--app/views/projects/_merge_request_merge_commit_template.html.haml17
-rw-r--r--app/views/projects/_merge_request_settings.html.haml2
-rw-r--r--app/views/projects/_new_project_fields.html.haml11
-rw-r--r--app/views/projects/_remove.html.haml4
-rw-r--r--app/views/projects/_remove_fork.html.haml2
-rw-r--r--app/views/projects/_service_desk_settings.html.haml3
-rw-r--r--app/views/projects/_transfer.html.haml2
-rw-r--r--app/views/projects/_visibility_modal.html.haml6
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_header.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml10
-rw-r--r--app/views/projects/blob/viewers/_changelog.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_readme.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml16
-rw-r--r--app/views/projects/commit/_multiple_signatures_signature_badge.html.haml6
-rw-r--r--app/views/projects/confluences/show.html.haml7
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml4
-rw-r--r--app/views/projects/deployments/_deployment.html.haml2
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/environments/index.html.haml25
-rw-r--r--app/views/projects/google_cloud/index.html.haml79
-rw-r--r--app/views/projects/hook_logs/show.html.haml16
-rw-r--r--app/views/projects/issues/_discussion.html.haml3
-rw-r--r--app/views/projects/issues/_issues.html.haml2
-rw-r--r--app/views/projects/issues/_service_desk_empty_state.html.haml2
-rw-r--r--app/views/projects/issues/_service_desk_info_content.html.haml5
-rw-r--r--app/views/projects/learn_gitlab/index.html.haml9
-rw-r--r--app/views/projects/merge_requests/_widget.html.haml4
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml2
-rw-r--r--app/views/projects/new.html.haml2
-rw-r--r--app/views/projects/pages/_no_domains.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_tabs.html.haml26
-rw-r--r--app/views/projects/pipelines/_info.html.haml79
-rw-r--r--app/views/projects/pipelines/charts.html.haml3
-rw-r--r--app/views/projects/pipelines/show.html.haml4
-rw-r--r--app/views/projects/product_analytics/_links.html.haml15
-rw-r--r--app/views/projects/settings/_general.html.haml7
-rw-r--r--app/views/projects/work_items/index.html.haml2
-rw-r--r--app/views/registrations/welcome/show.html.haml3
-rw-r--r--app/views/search/_category.html.haml2
-rw-r--r--app/views/shared/_confirm_modal.html.haml6
-rw-r--r--app/views/shared/_default_branch_protection.html.haml7
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/_md_preview.html.haml2
-rw-r--r--app/views/shared/_new_commit_form.html.haml4
-rw-r--r--app/views/shared/_no_password.html.haml16
-rw-r--r--app/views/shared/_no_ssh.html.haml14
-rw-r--r--app/views/shared/_project_creation_levels.html.haml7
-rw-r--r--app/views/shared/_visibility_radios.html.haml1
-rw-r--r--app/views/shared/alerts/_positioning_disabled.html.haml2
-rw-r--r--app/views/shared/boards/_switcher.html.haml7
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml2
-rw-r--r--app/views/shared/doorkeeper/applications/_show.html.haml9
-rw-r--r--app/views/shared/hook_logs/_content.html.haml59
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml1
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml2
-rw-r--r--app/views/shared/issuable/form/_type_selector.html.haml18
-rw-r--r--app/views/shared/labels/_nav.html.haml10
-rw-r--r--app/views/shared/nav/_sidebar_menu.html.haml13
-rw-r--r--app/views/shared/nav/_sidebar_submenu.html.haml12
-rw-r--r--app/views/shared/projects/_topics.html.haml4
-rw-r--r--app/views/shared/runners/_shared_runners_description.html.haml3
-rw-r--r--app/views/shared/topics/_list.html.haml9
-rw-r--r--app/views/shared/topics/_topic.html.haml16
-rw-r--r--app/views/shared/web_hooks/_index.html.haml2
-rw-r--r--app/views/users/_overview.html.haml17
-rw-r--r--app/views/users/calendar_activities.html.haml4
-rw-r--r--app/views/users/terms/index.html.haml37
-rw-r--r--app/workers/all_queues.yml130
-rw-r--r--app/workers/authorized_project_update/project_recalculate_worker.rb2
-rw-r--r--app/workers/authorized_projects_worker.rb4
-rw-r--r--app/workers/build_hooks_worker.rb2
-rw-r--r--app/workers/build_queue_worker.rb4
-rw-r--r--app/workers/build_success_worker.rb4
-rw-r--r--app/workers/chat_notification_worker.rb4
-rw-r--r--app/workers/ci/archive_trace_worker.rb4
-rw-r--r--app/workers/ci/build_finished_worker.rb2
-rw-r--r--app/workers/ci/build_trace_chunk_flush_worker.rb4
-rw-r--r--app/workers/ci/pipeline_success_unlock_artifacts_worker.rb5
-rw-r--r--app/workers/ci/ref_delete_unlock_artifacts_worker.rb5
-rw-r--r--app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb2
-rw-r--r--app/workers/cluster_update_app_worker.rb4
-rw-r--r--app/workers/clusters/applications/check_prometheus_health_worker.rb33
-rw-r--r--app/workers/clusters/integrations/check_prometheus_health_worker.rb32
-rw-r--r--app/workers/concerns/application_worker.rb95
-rw-r--r--app/workers/concerns/gitlab/github_import/rescheduling_methods.rb4
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb6
-rw-r--r--app/workers/concerns/gitlab/jira_import/import_worker.rb2
-rw-r--r--app/workers/concerns/limited_capacity/worker.rb2
-rw-r--r--app/workers/concerns/new_issuable.rb8
-rw-r--r--app/workers/concerns/todos_destroyer_queue.rb2
-rw-r--r--app/workers/container_expiration_policies/cleanup_container_repository_worker.rb5
-rw-r--r--app/workers/create_commit_signature_worker.rb4
-rw-r--r--app/workers/database/drop_detached_partitions_worker.rb2
-rw-r--r--app/workers/database/partition_management_worker.rb2
-rw-r--r--app/workers/delete_container_repository_worker.rb6
-rw-r--r--app/workers/dependency_proxy/image_ttl_group_policy_worker.rb5
-rw-r--r--app/workers/deployments/archive_in_project_worker.rb19
-rw-r--r--app/workers/detect_repository_languages_worker.rb4
-rw-r--r--app/workers/email_receiver_worker.rb6
-rw-r--r--app/workers/emails_on_push_worker.rb17
-rw-r--r--app/workers/expire_build_instance_artifacts_worker.rb6
-rw-r--r--app/workers/expire_job_cache_worker.rb4
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb4
-rw-r--r--app/workers/export_csv_worker.rb2
-rw-r--r--app/workers/gitlab/jira_import/import_issue_worker.rb4
-rw-r--r--app/workers/gitlab/jira_import/stage/start_import_worker.rb2
-rw-r--r--app/workers/hashed_storage/project_migrate_worker.rb4
-rw-r--r--app/workers/hashed_storage/project_rollback_worker.rb4
-rw-r--r--app/workers/import_issues_csv_worker.rb2
-rw-r--r--app/workers/integrations/create_external_cross_reference_worker.rb47
-rw-r--r--app/workers/invalid_gpg_signature_update_worker.rb4
-rw-r--r--app/workers/issuable/label_links_destroy_worker.rb2
-rw-r--r--app/workers/issuable_export_csv_worker.rb2
-rw-r--r--app/workers/issuables/clear_groups_issue_counter_worker.rb2
-rw-r--r--app/workers/issue_due_scheduler_worker.rb2
-rw-r--r--app/workers/issue_placement_worker.rb5
-rw-r--r--app/workers/issue_rebalancing_worker.rb6
-rw-r--r--app/workers/issues/placement_worker.rb67
-rw-r--r--app/workers/issues/rebalancing_worker.rb51
-rw-r--r--app/workers/issues/reschedule_stuck_issue_rebalances_worker.rb36
-rw-r--r--app/workers/loose_foreign_keys/cleanup_worker.rb25
-rw-r--r--app/workers/mail_scheduler/issue_due_worker.rb2
-rw-r--r--app/workers/mail_scheduler/notification_service_worker.rb2
-rw-r--r--app/workers/namespaces/invite_team_email_worker.rb22
-rw-r--r--app/workers/new_issue_worker.rb2
-rw-r--r--app/workers/new_note_worker.rb6
-rw-r--r--app/workers/pages_domain_verification_worker.rb4
-rw-r--r--app/workers/pages_worker.rb4
-rw-r--r--app/workers/pipeline_hooks_worker.rb4
-rw-r--r--app/workers/pipeline_metrics_worker.rb8
-rw-r--r--app/workers/pipeline_process_worker.rb4
-rw-r--r--app/workers/post_receive.rb1
-rw-r--r--app/workers/project_cache_worker.rb4
-rw-r--r--app/workers/propagate_service_template_worker.rb4
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb6
-rw-r--r--app/workers/ssh_keys/expired_notification_worker.rb4
-rw-r--r--app/workers/tasks_to_be_done/create_worker.rb31
-rw-r--r--app/workers/update_highest_role_worker.rb6
-rw-r--r--app/workers/update_merge_requests_worker.rb6
-rw-r--r--app/workers/update_project_statistics_worker.rb4
3231 files changed, 18337 insertions, 8009 deletions
diff --git a/app/assets/images/auth_buttons/dingtalk_64.png b/app/assets/images/auth_buttons/dingtalk_64.png
new file mode 100644
index 00000000000..77b3fa752bc
--- /dev/null
+++ b/app/assets/images/auth_buttons/dingtalk_64.png
Binary files differ
diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png
index 723c2c3f4c8..bc5041a165b 100644
--- a/app/assets/images/emoji.png
+++ b/app/assets/images/emoji.png
Binary files differ
diff --git a/app/assets/images/emoji/100.png b/app/assets/images/emoji/100.png
deleted file mode 100644
index 6903ff0304a..00000000000
--- a/app/assets/images/emoji/100.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/1234.png b/app/assets/images/emoji/1234.png
deleted file mode 100644
index 248dc7e55b6..00000000000
--- a/app/assets/images/emoji/1234.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/8ball.png b/app/assets/images/emoji/8ball.png
deleted file mode 100644
index 38ca662eded..00000000000
--- a/app/assets/images/emoji/8ball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/a.png b/app/assets/images/emoji/a.png
deleted file mode 100644
index 8603ff05a17..00000000000
--- a/app/assets/images/emoji/a.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ab.png b/app/assets/images/emoji/ab.png
deleted file mode 100644
index d9f2d17dea0..00000000000
--- a/app/assets/images/emoji/ab.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/abc.png b/app/assets/images/emoji/abc.png
deleted file mode 100644
index 7688de692a9..00000000000
--- a/app/assets/images/emoji/abc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/abcd.png b/app/assets/images/emoji/abcd.png
deleted file mode 100644
index 0996a870570..00000000000
--- a/app/assets/images/emoji/abcd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/accept.png b/app/assets/images/emoji/accept.png
deleted file mode 100644
index 8afd7ce99cf..00000000000
--- a/app/assets/images/emoji/accept.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/aerial_tramway.png b/app/assets/images/emoji/aerial_tramway.png
deleted file mode 100644
index 3eb4b61bf1d..00000000000
--- a/app/assets/images/emoji/aerial_tramway.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/airplane.png b/app/assets/images/emoji/airplane.png
deleted file mode 100644
index 268d2ac3c8e..00000000000
--- a/app/assets/images/emoji/airplane.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/airplane_arriving.png b/app/assets/images/emoji/airplane_arriving.png
deleted file mode 100644
index d66841962f2..00000000000
--- a/app/assets/images/emoji/airplane_arriving.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/airplane_departure.png b/app/assets/images/emoji/airplane_departure.png
deleted file mode 100644
index a5766f9f4ae..00000000000
--- a/app/assets/images/emoji/airplane_departure.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/airplane_small.png b/app/assets/images/emoji/airplane_small.png
deleted file mode 100644
index b731b15e3a8..00000000000
--- a/app/assets/images/emoji/airplane_small.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/alarm_clock.png b/app/assets/images/emoji/alarm_clock.png
deleted file mode 100644
index cdbc2fbb950..00000000000
--- a/app/assets/images/emoji/alarm_clock.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/alembic.png b/app/assets/images/emoji/alembic.png
deleted file mode 100644
index 307a7324249..00000000000
--- a/app/assets/images/emoji/alembic.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/alien.png b/app/assets/images/emoji/alien.png
deleted file mode 100644
index 3b90e97433b..00000000000
--- a/app/assets/images/emoji/alien.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ambulance.png b/app/assets/images/emoji/ambulance.png
deleted file mode 100644
index 6fb8076d766..00000000000
--- a/app/assets/images/emoji/ambulance.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/amphora.png b/app/assets/images/emoji/amphora.png
deleted file mode 100644
index 96de5056059..00000000000
--- a/app/assets/images/emoji/amphora.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/anchor.png b/app/assets/images/emoji/anchor.png
deleted file mode 100644
index b036f70a00b..00000000000
--- a/app/assets/images/emoji/anchor.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angel.png b/app/assets/images/emoji/angel.png
deleted file mode 100644
index 66ea97a3b99..00000000000
--- a/app/assets/images/emoji/angel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone1.png b/app/assets/images/emoji/angel_tone1.png
deleted file mode 100644
index 391694dc07e..00000000000
--- a/app/assets/images/emoji/angel_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone2.png b/app/assets/images/emoji/angel_tone2.png
deleted file mode 100644
index 700cbe6ed2c..00000000000
--- a/app/assets/images/emoji/angel_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone3.png b/app/assets/images/emoji/angel_tone3.png
deleted file mode 100644
index be597437d25..00000000000
--- a/app/assets/images/emoji/angel_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone4.png b/app/assets/images/emoji/angel_tone4.png
deleted file mode 100644
index b06d3c853ef..00000000000
--- a/app/assets/images/emoji/angel_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone5.png b/app/assets/images/emoji/angel_tone5.png
deleted file mode 100644
index 17bd677e334..00000000000
--- a/app/assets/images/emoji/angel_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/anger.png b/app/assets/images/emoji/anger.png
deleted file mode 100644
index d63c2e000e4..00000000000
--- a/app/assets/images/emoji/anger.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/anger_right.png b/app/assets/images/emoji/anger_right.png
deleted file mode 100644
index f5c97c4d297..00000000000
--- a/app/assets/images/emoji/anger_right.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angry.png b/app/assets/images/emoji/angry.png
deleted file mode 100644
index cfc4a6ecde5..00000000000
--- a/app/assets/images/emoji/angry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ant.png b/app/assets/images/emoji/ant.png
deleted file mode 100644
index 994127ed6b3..00000000000
--- a/app/assets/images/emoji/ant.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/apple.png b/app/assets/images/emoji/apple.png
deleted file mode 100644
index da650c60f62..00000000000
--- a/app/assets/images/emoji/apple.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/aquarius.png b/app/assets/images/emoji/aquarius.png
deleted file mode 100644
index 641a4f68889..00000000000
--- a/app/assets/images/emoji/aquarius.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/aries.png b/app/assets/images/emoji/aries.png
deleted file mode 100644
index 21a189d0ede..00000000000
--- a/app/assets/images/emoji/aries.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_backward.png b/app/assets/images/emoji/arrow_backward.png
deleted file mode 100644
index ee38e3b038e..00000000000
--- a/app/assets/images/emoji/arrow_backward.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_double_down.png b/app/assets/images/emoji/arrow_double_down.png
deleted file mode 100644
index 90193bfcb40..00000000000
--- a/app/assets/images/emoji/arrow_double_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_double_up.png b/app/assets/images/emoji/arrow_double_up.png
deleted file mode 100644
index 13543d5eef2..00000000000
--- a/app/assets/images/emoji/arrow_double_up.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_down.png b/app/assets/images/emoji/arrow_down.png
deleted file mode 100644
index b8eefd0b19f..00000000000
--- a/app/assets/images/emoji/arrow_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_down_small.png b/app/assets/images/emoji/arrow_down_small.png
deleted file mode 100644
index 5870b9a2241..00000000000
--- a/app/assets/images/emoji/arrow_down_small.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_forward.png b/app/assets/images/emoji/arrow_forward.png
deleted file mode 100644
index 4e2b682857c..00000000000
--- a/app/assets/images/emoji/arrow_forward.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_heading_down.png b/app/assets/images/emoji/arrow_heading_down.png
deleted file mode 100644
index 2d9d24bca80..00000000000
--- a/app/assets/images/emoji/arrow_heading_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_heading_up.png b/app/assets/images/emoji/arrow_heading_up.png
deleted file mode 100644
index f29bfcfc0de..00000000000
--- a/app/assets/images/emoji/arrow_heading_up.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_left.png b/app/assets/images/emoji/arrow_left.png
deleted file mode 100644
index 8c685e0a81b..00000000000
--- a/app/assets/images/emoji/arrow_left.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_lower_left.png b/app/assets/images/emoji/arrow_lower_left.png
deleted file mode 100644
index 88b37716078..00000000000
--- a/app/assets/images/emoji/arrow_lower_left.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_lower_right.png b/app/assets/images/emoji/arrow_lower_right.png
deleted file mode 100644
index 7e807da7392..00000000000
--- a/app/assets/images/emoji/arrow_lower_right.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_right.png b/app/assets/images/emoji/arrow_right.png
deleted file mode 100644
index 4755670b5cc..00000000000
--- a/app/assets/images/emoji/arrow_right.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_right_hook.png b/app/assets/images/emoji/arrow_right_hook.png
deleted file mode 100644
index e7258ad3268..00000000000
--- a/app/assets/images/emoji/arrow_right_hook.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_up.png b/app/assets/images/emoji/arrow_up.png
deleted file mode 100644
index af8218a87f7..00000000000
--- a/app/assets/images/emoji/arrow_up.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_up_down.png b/app/assets/images/emoji/arrow_up_down.png
deleted file mode 100644
index dfa32b97186..00000000000
--- a/app/assets/images/emoji/arrow_up_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_up_small.png b/app/assets/images/emoji/arrow_up_small.png
deleted file mode 100644
index 20a13dcd5cd..00000000000
--- a/app/assets/images/emoji/arrow_up_small.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_upper_left.png b/app/assets/images/emoji/arrow_upper_left.png
deleted file mode 100644
index f38718fbe34..00000000000
--- a/app/assets/images/emoji/arrow_upper_left.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_upper_right.png b/app/assets/images/emoji/arrow_upper_right.png
deleted file mode 100644
index c43e12d0f64..00000000000
--- a/app/assets/images/emoji/arrow_upper_right.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrows_clockwise.png b/app/assets/images/emoji/arrows_clockwise.png
deleted file mode 100644
index 26e49c38388..00000000000
--- a/app/assets/images/emoji/arrows_clockwise.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrows_counterclockwise.png b/app/assets/images/emoji/arrows_counterclockwise.png
deleted file mode 100644
index 8d06d8e0912..00000000000
--- a/app/assets/images/emoji/arrows_counterclockwise.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/art.png b/app/assets/images/emoji/art.png
deleted file mode 100644
index bd6afe9ff06..00000000000
--- a/app/assets/images/emoji/art.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/articulated_lorry.png b/app/assets/images/emoji/articulated_lorry.png
deleted file mode 100644
index c8217317132..00000000000
--- a/app/assets/images/emoji/articulated_lorry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/asterisk.png b/app/assets/images/emoji/asterisk.png
deleted file mode 100644
index 2f8e5113803..00000000000
--- a/app/assets/images/emoji/asterisk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/astonished.png b/app/assets/images/emoji/astonished.png
deleted file mode 100644
index bd0ac55ec8e..00000000000
--- a/app/assets/images/emoji/astonished.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/athletic_shoe.png b/app/assets/images/emoji/athletic_shoe.png
deleted file mode 100644
index 423fa07dd5d..00000000000
--- a/app/assets/images/emoji/athletic_shoe.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/atm.png b/app/assets/images/emoji/atm.png
deleted file mode 100644
index 4d935307b94..00000000000
--- a/app/assets/images/emoji/atm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/atom.png b/app/assets/images/emoji/atom.png
deleted file mode 100644
index 5f4567aa093..00000000000
--- a/app/assets/images/emoji/atom.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/avocado.png b/app/assets/images/emoji/avocado.png
deleted file mode 100644
index 06f0d124aed..00000000000
--- a/app/assets/images/emoji/avocado.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/b.png b/app/assets/images/emoji/b.png
deleted file mode 100644
index 25875bc6a14..00000000000
--- a/app/assets/images/emoji/b.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby.png b/app/assets/images/emoji/baby.png
deleted file mode 100644
index a4af92c63c7..00000000000
--- a/app/assets/images/emoji/baby.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_bottle.png b/app/assets/images/emoji/baby_bottle.png
deleted file mode 100644
index 2bd10524180..00000000000
--- a/app/assets/images/emoji/baby_bottle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_chick.png b/app/assets/images/emoji/baby_chick.png
deleted file mode 100644
index dccd96576ea..00000000000
--- a/app/assets/images/emoji/baby_chick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_symbol.png b/app/assets/images/emoji/baby_symbol.png
deleted file mode 100644
index 64a10b71710..00000000000
--- a/app/assets/images/emoji/baby_symbol.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone1.png b/app/assets/images/emoji/baby_tone1.png
deleted file mode 100644
index d20911d40db..00000000000
--- a/app/assets/images/emoji/baby_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone2.png b/app/assets/images/emoji/baby_tone2.png
deleted file mode 100644
index b0a9b30ed17..00000000000
--- a/app/assets/images/emoji/baby_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone3.png b/app/assets/images/emoji/baby_tone3.png
deleted file mode 100644
index 7de5286fac1..00000000000
--- a/app/assets/images/emoji/baby_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone4.png b/app/assets/images/emoji/baby_tone4.png
deleted file mode 100644
index 9b7a86ac615..00000000000
--- a/app/assets/images/emoji/baby_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone5.png b/app/assets/images/emoji/baby_tone5.png
deleted file mode 100644
index fe1be34cb88..00000000000
--- a/app/assets/images/emoji/baby_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/back.png b/app/assets/images/emoji/back.png
deleted file mode 100644
index d32c5d4f17f..00000000000
--- a/app/assets/images/emoji/back.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bacon.png b/app/assets/images/emoji/bacon.png
deleted file mode 100644
index f38a485fbe4..00000000000
--- a/app/assets/images/emoji/bacon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/badminton.png b/app/assets/images/emoji/badminton.png
deleted file mode 100644
index 7ba15708990..00000000000
--- a/app/assets/images/emoji/badminton.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baggage_claim.png b/app/assets/images/emoji/baggage_claim.png
deleted file mode 100644
index 409b593e78a..00000000000
--- a/app/assets/images/emoji/baggage_claim.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/balloon.png b/app/assets/images/emoji/balloon.png
deleted file mode 100644
index 07916fe6df1..00000000000
--- a/app/assets/images/emoji/balloon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ballot_box.png b/app/assets/images/emoji/ballot_box.png
deleted file mode 100644
index 9b6767aea9e..00000000000
--- a/app/assets/images/emoji/ballot_box.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ballot_box_with_check.png b/app/assets/images/emoji/ballot_box_with_check.png
deleted file mode 100644
index 284d9573847..00000000000
--- a/app/assets/images/emoji/ballot_box_with_check.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bamboo.png b/app/assets/images/emoji/bamboo.png
deleted file mode 100644
index 5d5e0e728a0..00000000000
--- a/app/assets/images/emoji/bamboo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/banana.png b/app/assets/images/emoji/banana.png
deleted file mode 100644
index f4987279580..00000000000
--- a/app/assets/images/emoji/banana.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bangbang.png b/app/assets/images/emoji/bangbang.png
deleted file mode 100644
index 58a9c528fca..00000000000
--- a/app/assets/images/emoji/bangbang.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bank.png b/app/assets/images/emoji/bank.png
deleted file mode 100644
index dffdcef36a1..00000000000
--- a/app/assets/images/emoji/bank.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bar_chart.png b/app/assets/images/emoji/bar_chart.png
deleted file mode 100644
index 53c89455008..00000000000
--- a/app/assets/images/emoji/bar_chart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/barber.png b/app/assets/images/emoji/barber.png
deleted file mode 100644
index 896f4d716cf..00000000000
--- a/app/assets/images/emoji/barber.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baseball.png b/app/assets/images/emoji/baseball.png
deleted file mode 100644
index f8463f1538b..00000000000
--- a/app/assets/images/emoji/baseball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball.png b/app/assets/images/emoji/basketball.png
deleted file mode 100644
index 64c76b79c6d..00000000000
--- a/app/assets/images/emoji/basketball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player.png b/app/assets/images/emoji/basketball_player.png
deleted file mode 100644
index 8ce90c5cad6..00000000000
--- a/app/assets/images/emoji/basketball_player.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone1.png b/app/assets/images/emoji/basketball_player_tone1.png
deleted file mode 100644
index cd12c7ab9bf..00000000000
--- a/app/assets/images/emoji/basketball_player_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone2.png b/app/assets/images/emoji/basketball_player_tone2.png
deleted file mode 100644
index f892fd596da..00000000000
--- a/app/assets/images/emoji/basketball_player_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone3.png b/app/assets/images/emoji/basketball_player_tone3.png
deleted file mode 100644
index e109997a91a..00000000000
--- a/app/assets/images/emoji/basketball_player_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone4.png b/app/assets/images/emoji/basketball_player_tone4.png
deleted file mode 100644
index 3b90b946af4..00000000000
--- a/app/assets/images/emoji/basketball_player_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone5.png b/app/assets/images/emoji/basketball_player_tone5.png
deleted file mode 100644
index bafed7828a7..00000000000
--- a/app/assets/images/emoji/basketball_player_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bat.png b/app/assets/images/emoji/bat.png
deleted file mode 100644
index 3152c047e00..00000000000
--- a/app/assets/images/emoji/bat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bath.png b/app/assets/images/emoji/bath.png
deleted file mode 100644
index 43fba5c8a28..00000000000
--- a/app/assets/images/emoji/bath.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone1.png b/app/assets/images/emoji/bath_tone1.png
deleted file mode 100644
index 2152eabf2f5..00000000000
--- a/app/assets/images/emoji/bath_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone2.png b/app/assets/images/emoji/bath_tone2.png
deleted file mode 100644
index 2102e6133e3..00000000000
--- a/app/assets/images/emoji/bath_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone3.png b/app/assets/images/emoji/bath_tone3.png
deleted file mode 100644
index fae66181e9f..00000000000
--- a/app/assets/images/emoji/bath_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone4.png b/app/assets/images/emoji/bath_tone4.png
deleted file mode 100644
index 1f8959d0d99..00000000000
--- a/app/assets/images/emoji/bath_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone5.png b/app/assets/images/emoji/bath_tone5.png
deleted file mode 100644
index c8a08e84f25..00000000000
--- a/app/assets/images/emoji/bath_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bathtub.png b/app/assets/images/emoji/bathtub.png
deleted file mode 100644
index 9a5f09361eb..00000000000
--- a/app/assets/images/emoji/bathtub.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/battery.png b/app/assets/images/emoji/battery.png
deleted file mode 100644
index f593e2bdb65..00000000000
--- a/app/assets/images/emoji/battery.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/beach.png b/app/assets/images/emoji/beach.png
deleted file mode 100644
index 69108c8ea10..00000000000
--- a/app/assets/images/emoji/beach.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/beach_umbrella.png b/app/assets/images/emoji/beach_umbrella.png
deleted file mode 100644
index 220a74f8132..00000000000
--- a/app/assets/images/emoji/beach_umbrella.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bear.png b/app/assets/images/emoji/bear.png
deleted file mode 100644
index 272d56bbbcc..00000000000
--- a/app/assets/images/emoji/bear.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bed.png b/app/assets/images/emoji/bed.png
deleted file mode 100644
index 86f964e245d..00000000000
--- a/app/assets/images/emoji/bed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bee.png b/app/assets/images/emoji/bee.png
deleted file mode 100644
index 46156060096..00000000000
--- a/app/assets/images/emoji/bee.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/beer.png b/app/assets/images/emoji/beer.png
deleted file mode 100644
index b6d73dc0b7a..00000000000
--- a/app/assets/images/emoji/beer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/beers.png b/app/assets/images/emoji/beers.png
deleted file mode 100644
index b55deb66b41..00000000000
--- a/app/assets/images/emoji/beers.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/beetle.png b/app/assets/images/emoji/beetle.png
deleted file mode 100644
index 3d93174d7fc..00000000000
--- a/app/assets/images/emoji/beetle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/beginner.png b/app/assets/images/emoji/beginner.png
deleted file mode 100644
index bc434fb7cb5..00000000000
--- a/app/assets/images/emoji/beginner.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bell.png b/app/assets/images/emoji/bell.png
deleted file mode 100644
index 5b3b0461999..00000000000
--- a/app/assets/images/emoji/bell.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bellhop.png b/app/assets/images/emoji/bellhop.png
deleted file mode 100644
index 6b3297ceaf7..00000000000
--- a/app/assets/images/emoji/bellhop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bento.png b/app/assets/images/emoji/bento.png
deleted file mode 100644
index 83d41ca7eb9..00000000000
--- a/app/assets/images/emoji/bento.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist.png b/app/assets/images/emoji/bicyclist.png
deleted file mode 100644
index 9274da11048..00000000000
--- a/app/assets/images/emoji/bicyclist.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone1.png b/app/assets/images/emoji/bicyclist_tone1.png
deleted file mode 100644
index decc2f728fe..00000000000
--- a/app/assets/images/emoji/bicyclist_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone2.png b/app/assets/images/emoji/bicyclist_tone2.png
deleted file mode 100644
index 0067717b80a..00000000000
--- a/app/assets/images/emoji/bicyclist_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone3.png b/app/assets/images/emoji/bicyclist_tone3.png
deleted file mode 100644
index a4f7b5e2776..00000000000
--- a/app/assets/images/emoji/bicyclist_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone4.png b/app/assets/images/emoji/bicyclist_tone4.png
deleted file mode 100644
index a3c8a797db4..00000000000
--- a/app/assets/images/emoji/bicyclist_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone5.png b/app/assets/images/emoji/bicyclist_tone5.png
deleted file mode 100644
index 1606a874051..00000000000
--- a/app/assets/images/emoji/bicyclist_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bike.png b/app/assets/images/emoji/bike.png
deleted file mode 100644
index 556ed70f1a7..00000000000
--- a/app/assets/images/emoji/bike.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bikini.png b/app/assets/images/emoji/bikini.png
deleted file mode 100644
index 77a8a0aae5b..00000000000
--- a/app/assets/images/emoji/bikini.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/biohazard.png b/app/assets/images/emoji/biohazard.png
deleted file mode 100644
index 007b4fc2d85..00000000000
--- a/app/assets/images/emoji/biohazard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bird.png b/app/assets/images/emoji/bird.png
deleted file mode 100644
index e201c22be33..00000000000
--- a/app/assets/images/emoji/bird.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/birthday.png b/app/assets/images/emoji/birthday.png
deleted file mode 100644
index 317e9a41949..00000000000
--- a/app/assets/images/emoji/birthday.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_circle.png b/app/assets/images/emoji/black_circle.png
deleted file mode 100644
index b62b87170e8..00000000000
--- a/app/assets/images/emoji/black_circle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_heart.png b/app/assets/images/emoji/black_heart.png
deleted file mode 100644
index b4068c3e6e8..00000000000
--- a/app/assets/images/emoji/black_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_joker.png b/app/assets/images/emoji/black_joker.png
deleted file mode 100644
index 3d0924b68aa..00000000000
--- a/app/assets/images/emoji/black_joker.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_large_square.png b/app/assets/images/emoji/black_large_square.png
deleted file mode 100644
index 162f2bb4290..00000000000
--- a/app/assets/images/emoji/black_large_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_medium_small_square.png b/app/assets/images/emoji/black_medium_small_square.png
deleted file mode 100644
index 39765bba610..00000000000
--- a/app/assets/images/emoji/black_medium_small_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_medium_square.png b/app/assets/images/emoji/black_medium_square.png
deleted file mode 100644
index 05a30a6aa2d..00000000000
--- a/app/assets/images/emoji/black_medium_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_nib.png b/app/assets/images/emoji/black_nib.png
deleted file mode 100644
index 872d0ae1598..00000000000
--- a/app/assets/images/emoji/black_nib.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_small_square.png b/app/assets/images/emoji/black_small_square.png
deleted file mode 100644
index 48595d3e1a9..00000000000
--- a/app/assets/images/emoji/black_small_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_square_button.png b/app/assets/images/emoji/black_square_button.png
deleted file mode 100644
index a78fc2f6b63..00000000000
--- a/app/assets/images/emoji/black_square_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/blossom.png b/app/assets/images/emoji/blossom.png
deleted file mode 100644
index 4083026c157..00000000000
--- a/app/assets/images/emoji/blossom.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/blowfish.png b/app/assets/images/emoji/blowfish.png
deleted file mode 100644
index a10f4f84e35..00000000000
--- a/app/assets/images/emoji/blowfish.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/blue_book.png b/app/assets/images/emoji/blue_book.png
deleted file mode 100644
index e1e455401cc..00000000000
--- a/app/assets/images/emoji/blue_book.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/blue_car.png b/app/assets/images/emoji/blue_car.png
deleted file mode 100644
index e8ba817d393..00000000000
--- a/app/assets/images/emoji/blue_car.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/blue_heart.png b/app/assets/images/emoji/blue_heart.png
deleted file mode 100644
index bdf1287e55e..00000000000
--- a/app/assets/images/emoji/blue_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/blush.png b/app/assets/images/emoji/blush.png
deleted file mode 100644
index aac1a424ad4..00000000000
--- a/app/assets/images/emoji/blush.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boar.png b/app/assets/images/emoji/boar.png
deleted file mode 100644
index fead972633c..00000000000
--- a/app/assets/images/emoji/boar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bomb.png b/app/assets/images/emoji/bomb.png
deleted file mode 100644
index c7f8f81c939..00000000000
--- a/app/assets/images/emoji/bomb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/book.png b/app/assets/images/emoji/book.png
deleted file mode 100644
index 0f4447ed396..00000000000
--- a/app/assets/images/emoji/book.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bookmark.png b/app/assets/images/emoji/bookmark.png
deleted file mode 100644
index bbb444611f0..00000000000
--- a/app/assets/images/emoji/bookmark.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bookmark_tabs.png b/app/assets/images/emoji/bookmark_tabs.png
deleted file mode 100644
index f8d9e01b428..00000000000
--- a/app/assets/images/emoji/bookmark_tabs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/books.png b/app/assets/images/emoji/books.png
deleted file mode 100644
index 59a8bafeb0d..00000000000
--- a/app/assets/images/emoji/books.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boom.png b/app/assets/images/emoji/boom.png
deleted file mode 100644
index 9b0f027b1a8..00000000000
--- a/app/assets/images/emoji/boom.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boot.png b/app/assets/images/emoji/boot.png
deleted file mode 100644
index 11f1065ed07..00000000000
--- a/app/assets/images/emoji/boot.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bouquet.png b/app/assets/images/emoji/bouquet.png
deleted file mode 100644
index 11455af6df4..00000000000
--- a/app/assets/images/emoji/bouquet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow.png b/app/assets/images/emoji/bow.png
deleted file mode 100644
index d8f793088dc..00000000000
--- a/app/assets/images/emoji/bow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow_and_arrow.png b/app/assets/images/emoji/bow_and_arrow.png
deleted file mode 100644
index 6a538bf475f..00000000000
--- a/app/assets/images/emoji/bow_and_arrow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone1.png b/app/assets/images/emoji/bow_tone1.png
deleted file mode 100644
index 87afb7b54cf..00000000000
--- a/app/assets/images/emoji/bow_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone2.png b/app/assets/images/emoji/bow_tone2.png
deleted file mode 100644
index 3ccf7dc0850..00000000000
--- a/app/assets/images/emoji/bow_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone3.png b/app/assets/images/emoji/bow_tone3.png
deleted file mode 100644
index 8b9eb64f926..00000000000
--- a/app/assets/images/emoji/bow_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone4.png b/app/assets/images/emoji/bow_tone4.png
deleted file mode 100644
index 683795ff40d..00000000000
--- a/app/assets/images/emoji/bow_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone5.png b/app/assets/images/emoji/bow_tone5.png
deleted file mode 100644
index 7969d971752..00000000000
--- a/app/assets/images/emoji/bow_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bowling.png b/app/assets/images/emoji/bowling.png
deleted file mode 100644
index 63add89e53b..00000000000
--- a/app/assets/images/emoji/bowling.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boxing_glove.png b/app/assets/images/emoji/boxing_glove.png
deleted file mode 100644
index 9838f24e51a..00000000000
--- a/app/assets/images/emoji/boxing_glove.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boy.png b/app/assets/images/emoji/boy.png
deleted file mode 100644
index 8ecfb0a4e92..00000000000
--- a/app/assets/images/emoji/boy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone1.png b/app/assets/images/emoji/boy_tone1.png
deleted file mode 100644
index 2fc436ea512..00000000000
--- a/app/assets/images/emoji/boy_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone2.png b/app/assets/images/emoji/boy_tone2.png
deleted file mode 100644
index 09a5f18d360..00000000000
--- a/app/assets/images/emoji/boy_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone3.png b/app/assets/images/emoji/boy_tone3.png
deleted file mode 100644
index 3cfe675dd3a..00000000000
--- a/app/assets/images/emoji/boy_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone4.png b/app/assets/images/emoji/boy_tone4.png
deleted file mode 100644
index 780be0ace36..00000000000
--- a/app/assets/images/emoji/boy_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone5.png b/app/assets/images/emoji/boy_tone5.png
deleted file mode 100644
index f32fe22e35c..00000000000
--- a/app/assets/images/emoji/boy_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bread.png b/app/assets/images/emoji/bread.png
deleted file mode 100644
index 6676510aaa5..00000000000
--- a/app/assets/images/emoji/bread.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil.png b/app/assets/images/emoji/bride_with_veil.png
deleted file mode 100644
index eaf4bd97890..00000000000
--- a/app/assets/images/emoji/bride_with_veil.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone1.png b/app/assets/images/emoji/bride_with_veil_tone1.png
deleted file mode 100644
index c4fb141ae8f..00000000000
--- a/app/assets/images/emoji/bride_with_veil_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone2.png b/app/assets/images/emoji/bride_with_veil_tone2.png
deleted file mode 100644
index c248769fc06..00000000000
--- a/app/assets/images/emoji/bride_with_veil_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone3.png b/app/assets/images/emoji/bride_with_veil_tone3.png
deleted file mode 100644
index 962c0a6eedb..00000000000
--- a/app/assets/images/emoji/bride_with_veil_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone4.png b/app/assets/images/emoji/bride_with_veil_tone4.png
deleted file mode 100644
index 740ca208cd4..00000000000
--- a/app/assets/images/emoji/bride_with_veil_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone5.png b/app/assets/images/emoji/bride_with_veil_tone5.png
deleted file mode 100644
index 5cc5598587d..00000000000
--- a/app/assets/images/emoji/bride_with_veil_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bridge_at_night.png b/app/assets/images/emoji/bridge_at_night.png
deleted file mode 100644
index 1d444e0be65..00000000000
--- a/app/assets/images/emoji/bridge_at_night.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/briefcase.png b/app/assets/images/emoji/briefcase.png
deleted file mode 100644
index b9912ba2148..00000000000
--- a/app/assets/images/emoji/briefcase.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/broken_heart.png b/app/assets/images/emoji/broken_heart.png
deleted file mode 100644
index 718e26ee122..00000000000
--- a/app/assets/images/emoji/broken_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bug.png b/app/assets/images/emoji/bug.png
deleted file mode 100644
index e64e72f259a..00000000000
--- a/app/assets/images/emoji/bug.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bulb.png b/app/assets/images/emoji/bulb.png
deleted file mode 100644
index 38e32e02d9f..00000000000
--- a/app/assets/images/emoji/bulb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bullettrain_front.png b/app/assets/images/emoji/bullettrain_front.png
deleted file mode 100644
index 4f698e056fa..00000000000
--- a/app/assets/images/emoji/bullettrain_front.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bullettrain_side.png b/app/assets/images/emoji/bullettrain_side.png
deleted file mode 100644
index ed61c67bf07..00000000000
--- a/app/assets/images/emoji/bullettrain_side.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/burrito.png b/app/assets/images/emoji/burrito.png
deleted file mode 100644
index 02bd5601df7..00000000000
--- a/app/assets/images/emoji/burrito.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bus.png b/app/assets/images/emoji/bus.png
deleted file mode 100644
index 641ddc56ca7..00000000000
--- a/app/assets/images/emoji/bus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/busstop.png b/app/assets/images/emoji/busstop.png
deleted file mode 100644
index b2b62208bfd..00000000000
--- a/app/assets/images/emoji/busstop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bust_in_silhouette.png b/app/assets/images/emoji/bust_in_silhouette.png
deleted file mode 100644
index 123b2cbe1fb..00000000000
--- a/app/assets/images/emoji/bust_in_silhouette.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/busts_in_silhouette.png b/app/assets/images/emoji/busts_in_silhouette.png
deleted file mode 100644
index d7656860a1c..00000000000
--- a/app/assets/images/emoji/busts_in_silhouette.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/butterfly.png b/app/assets/images/emoji/butterfly.png
deleted file mode 100644
index 5631fe99226..00000000000
--- a/app/assets/images/emoji/butterfly.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cactus.png b/app/assets/images/emoji/cactus.png
deleted file mode 100644
index 9b48ccf3d0c..00000000000
--- a/app/assets/images/emoji/cactus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cake.png b/app/assets/images/emoji/cake.png
deleted file mode 100644
index 4368177be9a..00000000000
--- a/app/assets/images/emoji/cake.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/calendar.png b/app/assets/images/emoji/calendar.png
deleted file mode 100644
index 47353b74447..00000000000
--- a/app/assets/images/emoji/calendar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/calendar_spiral.png b/app/assets/images/emoji/calendar_spiral.png
deleted file mode 100644
index dec8d49bfa8..00000000000
--- a/app/assets/images/emoji/calendar_spiral.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/call_me.png b/app/assets/images/emoji/call_me.png
deleted file mode 100644
index a10c59ba711..00000000000
--- a/app/assets/images/emoji/call_me.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone1.png b/app/assets/images/emoji/call_me_tone1.png
deleted file mode 100644
index 2c93201181a..00000000000
--- a/app/assets/images/emoji/call_me_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone2.png b/app/assets/images/emoji/call_me_tone2.png
deleted file mode 100644
index c39f45a41ed..00000000000
--- a/app/assets/images/emoji/call_me_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone3.png b/app/assets/images/emoji/call_me_tone3.png
deleted file mode 100644
index 83a57f63c29..00000000000
--- a/app/assets/images/emoji/call_me_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone4.png b/app/assets/images/emoji/call_me_tone4.png
deleted file mode 100644
index 65b3468fe44..00000000000
--- a/app/assets/images/emoji/call_me_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone5.png b/app/assets/images/emoji/call_me_tone5.png
deleted file mode 100644
index 94ef68ff3b3..00000000000
--- a/app/assets/images/emoji/call_me_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/calling.png b/app/assets/images/emoji/calling.png
deleted file mode 100644
index e2f308f8e46..00000000000
--- a/app/assets/images/emoji/calling.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/camel.png b/app/assets/images/emoji/camel.png
deleted file mode 100644
index b421d07a805..00000000000
--- a/app/assets/images/emoji/camel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/camera.png b/app/assets/images/emoji/camera.png
deleted file mode 100644
index 0a3429f72ef..00000000000
--- a/app/assets/images/emoji/camera.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/camera_with_flash.png b/app/assets/images/emoji/camera_with_flash.png
deleted file mode 100644
index 27471da2029..00000000000
--- a/app/assets/images/emoji/camera_with_flash.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/camping.png b/app/assets/images/emoji/camping.png
deleted file mode 100644
index d589cc1f44b..00000000000
--- a/app/assets/images/emoji/camping.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cancer.png b/app/assets/images/emoji/cancer.png
deleted file mode 100644
index a64af07cb5f..00000000000
--- a/app/assets/images/emoji/cancer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/candle.png b/app/assets/images/emoji/candle.png
deleted file mode 100644
index 0b56444e355..00000000000
--- a/app/assets/images/emoji/candle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/candy.png b/app/assets/images/emoji/candy.png
deleted file mode 100644
index 8c67ace3a35..00000000000
--- a/app/assets/images/emoji/candy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/canoe.png b/app/assets/images/emoji/canoe.png
deleted file mode 100644
index e26cdb9da69..00000000000
--- a/app/assets/images/emoji/canoe.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/capital_abcd.png b/app/assets/images/emoji/capital_abcd.png
deleted file mode 100644
index fe9482d2d8a..00000000000
--- a/app/assets/images/emoji/capital_abcd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/capricorn.png b/app/assets/images/emoji/capricorn.png
deleted file mode 100644
index 6293d31d4b1..00000000000
--- a/app/assets/images/emoji/capricorn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/card_box.png b/app/assets/images/emoji/card_box.png
deleted file mode 100644
index f2e764ce59d..00000000000
--- a/app/assets/images/emoji/card_box.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/card_index.png b/app/assets/images/emoji/card_index.png
deleted file mode 100644
index 151e11cb3b4..00000000000
--- a/app/assets/images/emoji/card_index.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/carousel_horse.png b/app/assets/images/emoji/carousel_horse.png
deleted file mode 100644
index a17074edf05..00000000000
--- a/app/assets/images/emoji/carousel_horse.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/carrot.png b/app/assets/images/emoji/carrot.png
deleted file mode 100644
index c68829b58e7..00000000000
--- a/app/assets/images/emoji/carrot.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel.png b/app/assets/images/emoji/cartwheel.png
deleted file mode 100644
index cbcaa578253..00000000000
--- a/app/assets/images/emoji/cartwheel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone1.png b/app/assets/images/emoji/cartwheel_tone1.png
deleted file mode 100644
index db6d65895fb..00000000000
--- a/app/assets/images/emoji/cartwheel_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone2.png b/app/assets/images/emoji/cartwheel_tone2.png
deleted file mode 100644
index e00ffbc27a8..00000000000
--- a/app/assets/images/emoji/cartwheel_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone3.png b/app/assets/images/emoji/cartwheel_tone3.png
deleted file mode 100644
index 49321be391f..00000000000
--- a/app/assets/images/emoji/cartwheel_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone4.png b/app/assets/images/emoji/cartwheel_tone4.png
deleted file mode 100644
index d4562b5e3dd..00000000000
--- a/app/assets/images/emoji/cartwheel_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone5.png b/app/assets/images/emoji/cartwheel_tone5.png
deleted file mode 100644
index 6e09a870767..00000000000
--- a/app/assets/images/emoji/cartwheel_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cat.png b/app/assets/images/emoji/cat.png
deleted file mode 100644
index efd82c2abf3..00000000000
--- a/app/assets/images/emoji/cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cat2.png b/app/assets/images/emoji/cat2.png
deleted file mode 100644
index 46abe8cbc14..00000000000
--- a/app/assets/images/emoji/cat2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cd.png b/app/assets/images/emoji/cd.png
deleted file mode 100644
index e6b01449cd9..00000000000
--- a/app/assets/images/emoji/cd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chains.png b/app/assets/images/emoji/chains.png
deleted file mode 100644
index 57f46139a06..00000000000
--- a/app/assets/images/emoji/chains.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/champagne.png b/app/assets/images/emoji/champagne.png
deleted file mode 100644
index 285a79a93d0..00000000000
--- a/app/assets/images/emoji/champagne.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/champagne_glass.png b/app/assets/images/emoji/champagne_glass.png
deleted file mode 100644
index 31937ae9392..00000000000
--- a/app/assets/images/emoji/champagne_glass.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chart.png b/app/assets/images/emoji/chart.png
deleted file mode 100644
index 9773f03be22..00000000000
--- a/app/assets/images/emoji/chart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chart_with_downwards_trend.png b/app/assets/images/emoji/chart_with_downwards_trend.png
deleted file mode 100644
index 5222ec72d85..00000000000
--- a/app/assets/images/emoji/chart_with_downwards_trend.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chart_with_upwards_trend.png b/app/assets/images/emoji/chart_with_upwards_trend.png
deleted file mode 100644
index f13cfcf9956..00000000000
--- a/app/assets/images/emoji/chart_with_upwards_trend.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/checkered_flag.png b/app/assets/images/emoji/checkered_flag.png
deleted file mode 100644
index 5a71eecb89b..00000000000
--- a/app/assets/images/emoji/checkered_flag.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cheese.png b/app/assets/images/emoji/cheese.png
deleted file mode 100644
index 00e99762286..00000000000
--- a/app/assets/images/emoji/cheese.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cherries.png b/app/assets/images/emoji/cherries.png
deleted file mode 100644
index 9b10cbaac5e..00000000000
--- a/app/assets/images/emoji/cherries.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cherry_blossom.png b/app/assets/images/emoji/cherry_blossom.png
deleted file mode 100644
index 282f3e7bc81..00000000000
--- a/app/assets/images/emoji/cherry_blossom.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chestnut.png b/app/assets/images/emoji/chestnut.png
deleted file mode 100644
index e9fb40468ed..00000000000
--- a/app/assets/images/emoji/chestnut.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chicken.png b/app/assets/images/emoji/chicken.png
deleted file mode 100644
index 9a6992e55ba..00000000000
--- a/app/assets/images/emoji/chicken.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/children_crossing.png b/app/assets/images/emoji/children_crossing.png
deleted file mode 100644
index fa4c091c7c3..00000000000
--- a/app/assets/images/emoji/children_crossing.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chipmunk.png b/app/assets/images/emoji/chipmunk.png
deleted file mode 100644
index 2aac560cb22..00000000000
--- a/app/assets/images/emoji/chipmunk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chocolate_bar.png b/app/assets/images/emoji/chocolate_bar.png
deleted file mode 100644
index 318bbd40ef9..00000000000
--- a/app/assets/images/emoji/chocolate_bar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/christmas_tree.png b/app/assets/images/emoji/christmas_tree.png
deleted file mode 100644
index 4197d37a52b..00000000000
--- a/app/assets/images/emoji/christmas_tree.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/church.png b/app/assets/images/emoji/church.png
deleted file mode 100644
index 8242fd272b3..00000000000
--- a/app/assets/images/emoji/church.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cinema.png b/app/assets/images/emoji/cinema.png
deleted file mode 100644
index 65f27b386f2..00000000000
--- a/app/assets/images/emoji/cinema.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/circus_tent.png b/app/assets/images/emoji/circus_tent.png
deleted file mode 100644
index b0379775b12..00000000000
--- a/app/assets/images/emoji/circus_tent.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/city_dusk.png b/app/assets/images/emoji/city_dusk.png
deleted file mode 100644
index 80cdff7cf5d..00000000000
--- a/app/assets/images/emoji/city_dusk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/city_sunset.png b/app/assets/images/emoji/city_sunset.png
deleted file mode 100644
index 7cded0ba55b..00000000000
--- a/app/assets/images/emoji/city_sunset.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cityscape.png b/app/assets/images/emoji/cityscape.png
deleted file mode 100644
index d7b9844a0b4..00000000000
--- a/app/assets/images/emoji/cityscape.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cl.png b/app/assets/images/emoji/cl.png
deleted file mode 100644
index 8b01b4343e2..00000000000
--- a/app/assets/images/emoji/cl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clap.png b/app/assets/images/emoji/clap.png
deleted file mode 100644
index b0ffe928920..00000000000
--- a/app/assets/images/emoji/clap.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone1.png b/app/assets/images/emoji/clap_tone1.png
deleted file mode 100644
index de4bc837b96..00000000000
--- a/app/assets/images/emoji/clap_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone2.png b/app/assets/images/emoji/clap_tone2.png
deleted file mode 100644
index 1323de775ba..00000000000
--- a/app/assets/images/emoji/clap_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone3.png b/app/assets/images/emoji/clap_tone3.png
deleted file mode 100644
index d448ca19dde..00000000000
--- a/app/assets/images/emoji/clap_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone4.png b/app/assets/images/emoji/clap_tone4.png
deleted file mode 100644
index c49f44ee91d..00000000000
--- a/app/assets/images/emoji/clap_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone5.png b/app/assets/images/emoji/clap_tone5.png
deleted file mode 100644
index 29ee9bdf37c..00000000000
--- a/app/assets/images/emoji/clap_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clapper.png b/app/assets/images/emoji/clapper.png
deleted file mode 100644
index 81390883111..00000000000
--- a/app/assets/images/emoji/clapper.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/classical_building.png b/app/assets/images/emoji/classical_building.png
deleted file mode 100644
index de7b559daaf..00000000000
--- a/app/assets/images/emoji/classical_building.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clipboard.png b/app/assets/images/emoji/clipboard.png
deleted file mode 100644
index 7edcfc52509..00000000000
--- a/app/assets/images/emoji/clipboard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock.png b/app/assets/images/emoji/clock.png
deleted file mode 100644
index ffdb451e3a8..00000000000
--- a/app/assets/images/emoji/clock.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock1.png b/app/assets/images/emoji/clock1.png
deleted file mode 100644
index d6e34941f23..00000000000
--- a/app/assets/images/emoji/clock1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock10.png b/app/assets/images/emoji/clock10.png
deleted file mode 100644
index e62b245cdbe..00000000000
--- a/app/assets/images/emoji/clock10.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock1030.png b/app/assets/images/emoji/clock1030.png
deleted file mode 100644
index 0802b3c65b9..00000000000
--- a/app/assets/images/emoji/clock1030.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock11.png b/app/assets/images/emoji/clock11.png
deleted file mode 100644
index 0983345273b..00000000000
--- a/app/assets/images/emoji/clock11.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock1130.png b/app/assets/images/emoji/clock1130.png
deleted file mode 100644
index d970d03b809..00000000000
--- a/app/assets/images/emoji/clock1130.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock12.png b/app/assets/images/emoji/clock12.png
deleted file mode 100644
index e61caa4b3e2..00000000000
--- a/app/assets/images/emoji/clock12.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock1230.png b/app/assets/images/emoji/clock1230.png
deleted file mode 100644
index f2b1d261721..00000000000
--- a/app/assets/images/emoji/clock1230.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock130.png b/app/assets/images/emoji/clock130.png
deleted file mode 100644
index 86b7689b84e..00000000000
--- a/app/assets/images/emoji/clock130.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock2.png b/app/assets/images/emoji/clock2.png
deleted file mode 100644
index a54253d7d57..00000000000
--- a/app/assets/images/emoji/clock2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock230.png b/app/assets/images/emoji/clock230.png
deleted file mode 100644
index 7a787e018e6..00000000000
--- a/app/assets/images/emoji/clock230.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock3.png b/app/assets/images/emoji/clock3.png
deleted file mode 100644
index 27ec4b1f514..00000000000
--- a/app/assets/images/emoji/clock3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock330.png b/app/assets/images/emoji/clock330.png
deleted file mode 100644
index c6860395cec..00000000000
--- a/app/assets/images/emoji/clock330.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock4.png b/app/assets/images/emoji/clock4.png
deleted file mode 100644
index 60a1ef4cc13..00000000000
--- a/app/assets/images/emoji/clock4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock430.png b/app/assets/images/emoji/clock430.png
deleted file mode 100644
index 3c05b362122..00000000000
--- a/app/assets/images/emoji/clock430.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock5.png b/app/assets/images/emoji/clock5.png
deleted file mode 100644
index c9382d1e094..00000000000
--- a/app/assets/images/emoji/clock5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock530.png b/app/assets/images/emoji/clock530.png
deleted file mode 100644
index c21fa926db2..00000000000
--- a/app/assets/images/emoji/clock530.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock6.png b/app/assets/images/emoji/clock6.png
deleted file mode 100644
index 8fd5d3f5bd7..00000000000
--- a/app/assets/images/emoji/clock6.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock630.png b/app/assets/images/emoji/clock630.png
deleted file mode 100644
index 2aec87fefcf..00000000000
--- a/app/assets/images/emoji/clock630.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock7.png b/app/assets/images/emoji/clock7.png
deleted file mode 100644
index 8c7084036f2..00000000000
--- a/app/assets/images/emoji/clock7.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock730.png b/app/assets/images/emoji/clock730.png
deleted file mode 100644
index f7a1135e03f..00000000000
--- a/app/assets/images/emoji/clock730.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock8.png b/app/assets/images/emoji/clock8.png
deleted file mode 100644
index fcddf722e95..00000000000
--- a/app/assets/images/emoji/clock8.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock830.png b/app/assets/images/emoji/clock830.png
deleted file mode 100644
index 799b4aebc08..00000000000
--- a/app/assets/images/emoji/clock830.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock9.png b/app/assets/images/emoji/clock9.png
deleted file mode 100644
index dfbe0117981..00000000000
--- a/app/assets/images/emoji/clock9.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock930.png b/app/assets/images/emoji/clock930.png
deleted file mode 100644
index 4a2092ee6f0..00000000000
--- a/app/assets/images/emoji/clock930.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/closed_book.png b/app/assets/images/emoji/closed_book.png
deleted file mode 100644
index 6395cf2151e..00000000000
--- a/app/assets/images/emoji/closed_book.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/closed_lock_with_key.png b/app/assets/images/emoji/closed_lock_with_key.png
deleted file mode 100644
index 1c1cd5d0741..00000000000
--- a/app/assets/images/emoji/closed_lock_with_key.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/closed_umbrella.png b/app/assets/images/emoji/closed_umbrella.png
deleted file mode 100644
index ecefba9e446..00000000000
--- a/app/assets/images/emoji/closed_umbrella.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cloud.png b/app/assets/images/emoji/cloud.png
deleted file mode 100644
index 5b4f57f77ba..00000000000
--- a/app/assets/images/emoji/cloud.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cloud_lightning.png b/app/assets/images/emoji/cloud_lightning.png
deleted file mode 100644
index 0831e88aa31..00000000000
--- a/app/assets/images/emoji/cloud_lightning.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cloud_rain.png b/app/assets/images/emoji/cloud_rain.png
deleted file mode 100644
index 385685e0512..00000000000
--- a/app/assets/images/emoji/cloud_rain.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cloud_snow.png b/app/assets/images/emoji/cloud_snow.png
deleted file mode 100644
index 9720384eb99..00000000000
--- a/app/assets/images/emoji/cloud_snow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cloud_tornado.png b/app/assets/images/emoji/cloud_tornado.png
deleted file mode 100644
index 4821c89da1e..00000000000
--- a/app/assets/images/emoji/cloud_tornado.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clown.png b/app/assets/images/emoji/clown.png
deleted file mode 100644
index 02b7ff70049..00000000000
--- a/app/assets/images/emoji/clown.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clubs.png b/app/assets/images/emoji/clubs.png
deleted file mode 100644
index 4f2abf791ca..00000000000
--- a/app/assets/images/emoji/clubs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cocktail.png b/app/assets/images/emoji/cocktail.png
deleted file mode 100644
index 2e50c57e98d..00000000000
--- a/app/assets/images/emoji/cocktail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/coffee.png b/app/assets/images/emoji/coffee.png
deleted file mode 100644
index 553061471b1..00000000000
--- a/app/assets/images/emoji/coffee.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/coffin.png b/app/assets/images/emoji/coffin.png
deleted file mode 100644
index fb2932aa5f6..00000000000
--- a/app/assets/images/emoji/coffin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cold_sweat.png b/app/assets/images/emoji/cold_sweat.png
deleted file mode 100644
index 85b2231bbf6..00000000000
--- a/app/assets/images/emoji/cold_sweat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/comet.png b/app/assets/images/emoji/comet.png
deleted file mode 100644
index a99751f79be..00000000000
--- a/app/assets/images/emoji/comet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/compression.png b/app/assets/images/emoji/compression.png
deleted file mode 100644
index d7eda7f362a..00000000000
--- a/app/assets/images/emoji/compression.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/computer.png b/app/assets/images/emoji/computer.png
deleted file mode 100644
index c1fee27e3a9..00000000000
--- a/app/assets/images/emoji/computer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/confetti_ball.png b/app/assets/images/emoji/confetti_ball.png
deleted file mode 100644
index ba4fd9b12be..00000000000
--- a/app/assets/images/emoji/confetti_ball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/confounded.png b/app/assets/images/emoji/confounded.png
deleted file mode 100644
index aa4b29e9375..00000000000
--- a/app/assets/images/emoji/confounded.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/confused.png b/app/assets/images/emoji/confused.png
deleted file mode 100644
index 502b6bf0e0b..00000000000
--- a/app/assets/images/emoji/confused.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/congratulations.png b/app/assets/images/emoji/congratulations.png
deleted file mode 100644
index ba8c89d95ee..00000000000
--- a/app/assets/images/emoji/congratulations.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction.png b/app/assets/images/emoji/construction.png
deleted file mode 100644
index ef8db5f471c..00000000000
--- a/app/assets/images/emoji/construction.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_site.png b/app/assets/images/emoji/construction_site.png
deleted file mode 100644
index 8206a20f63f..00000000000
--- a/app/assets/images/emoji/construction_site.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker.png b/app/assets/images/emoji/construction_worker.png
deleted file mode 100644
index a9970a89005..00000000000
--- a/app/assets/images/emoji/construction_worker.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone1.png b/app/assets/images/emoji/construction_worker_tone1.png
deleted file mode 100644
index 2f24a2bab24..00000000000
--- a/app/assets/images/emoji/construction_worker_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone2.png b/app/assets/images/emoji/construction_worker_tone2.png
deleted file mode 100644
index 93c8fec5a75..00000000000
--- a/app/assets/images/emoji/construction_worker_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone3.png b/app/assets/images/emoji/construction_worker_tone3.png
deleted file mode 100644
index abc1f2af2e0..00000000000
--- a/app/assets/images/emoji/construction_worker_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone4.png b/app/assets/images/emoji/construction_worker_tone4.png
deleted file mode 100644
index eed83289aeb..00000000000
--- a/app/assets/images/emoji/construction_worker_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone5.png b/app/assets/images/emoji/construction_worker_tone5.png
deleted file mode 100644
index acbb220b8bb..00000000000
--- a/app/assets/images/emoji/construction_worker_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/control_knobs.png b/app/assets/images/emoji/control_knobs.png
deleted file mode 100644
index 6635ac93b50..00000000000
--- a/app/assets/images/emoji/control_knobs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/convenience_store.png b/app/assets/images/emoji/convenience_store.png
deleted file mode 100644
index 26b53b5669e..00000000000
--- a/app/assets/images/emoji/convenience_store.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cookie.png b/app/assets/images/emoji/cookie.png
deleted file mode 100644
index 1b6bcb1554f..00000000000
--- a/app/assets/images/emoji/cookie.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cooking.png b/app/assets/images/emoji/cooking.png
deleted file mode 100644
index 918c980577a..00000000000
--- a/app/assets/images/emoji/cooking.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cool.png b/app/assets/images/emoji/cool.png
deleted file mode 100644
index 74674978d00..00000000000
--- a/app/assets/images/emoji/cool.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cop.png b/app/assets/images/emoji/cop.png
deleted file mode 100644
index 0b16d7c17b7..00000000000
--- a/app/assets/images/emoji/cop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone1.png b/app/assets/images/emoji/cop_tone1.png
deleted file mode 100644
index 6ccba3879dc..00000000000
--- a/app/assets/images/emoji/cop_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone2.png b/app/assets/images/emoji/cop_tone2.png
deleted file mode 100644
index 7814ea9f52d..00000000000
--- a/app/assets/images/emoji/cop_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone3.png b/app/assets/images/emoji/cop_tone3.png
deleted file mode 100644
index d78e88ec872..00000000000
--- a/app/assets/images/emoji/cop_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone4.png b/app/assets/images/emoji/cop_tone4.png
deleted file mode 100644
index 2e13c508315..00000000000
--- a/app/assets/images/emoji/cop_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone5.png b/app/assets/images/emoji/cop_tone5.png
deleted file mode 100644
index 2980d61cc2e..00000000000
--- a/app/assets/images/emoji/cop_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/copyright.png b/app/assets/images/emoji/copyright.png
deleted file mode 100644
index 6b9a6adbfd2..00000000000
--- a/app/assets/images/emoji/copyright.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/corn.png b/app/assets/images/emoji/corn.png
deleted file mode 100644
index 36e20127931..00000000000
--- a/app/assets/images/emoji/corn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/couch.png b/app/assets/images/emoji/couch.png
deleted file mode 100644
index 27b19b13bb0..00000000000
--- a/app/assets/images/emoji/couch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/couple.png b/app/assets/images/emoji/couple.png
deleted file mode 100644
index 960323f3c16..00000000000
--- a/app/assets/images/emoji/couple.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/couple_mm.png b/app/assets/images/emoji/couple_mm.png
deleted file mode 100644
index 8759fa5db87..00000000000
--- a/app/assets/images/emoji/couple_mm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/couple_with_heart.png b/app/assets/images/emoji/couple_with_heart.png
deleted file mode 100644
index 62111601b36..00000000000
--- a/app/assets/images/emoji/couple_with_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/couple_ww.png b/app/assets/images/emoji/couple_ww.png
deleted file mode 100644
index 08fdabcdc5c..00000000000
--- a/app/assets/images/emoji/couple_ww.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/couplekiss.png b/app/assets/images/emoji/couplekiss.png
deleted file mode 100644
index 9aa519da9e8..00000000000
--- a/app/assets/images/emoji/couplekiss.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cow.png b/app/assets/images/emoji/cow.png
deleted file mode 100644
index 718a3986d64..00000000000
--- a/app/assets/images/emoji/cow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cow2.png b/app/assets/images/emoji/cow2.png
deleted file mode 100644
index 4d0ca534ff1..00000000000
--- a/app/assets/images/emoji/cow2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cowboy.png b/app/assets/images/emoji/cowboy.png
deleted file mode 100644
index 70dd5d0d9d1..00000000000
--- a/app/assets/images/emoji/cowboy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crab.png b/app/assets/images/emoji/crab.png
deleted file mode 100644
index 19f3047ab61..00000000000
--- a/app/assets/images/emoji/crab.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crayon.png b/app/assets/images/emoji/crayon.png
deleted file mode 100644
index 8d7b427aaa3..00000000000
--- a/app/assets/images/emoji/crayon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/credit_card.png b/app/assets/images/emoji/credit_card.png
deleted file mode 100644
index 372777d5c61..00000000000
--- a/app/assets/images/emoji/credit_card.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crescent_moon.png b/app/assets/images/emoji/crescent_moon.png
deleted file mode 100644
index 765420ecec7..00000000000
--- a/app/assets/images/emoji/crescent_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cricket.png b/app/assets/images/emoji/cricket.png
deleted file mode 100644
index d602294a2cd..00000000000
--- a/app/assets/images/emoji/cricket.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crocodile.png b/app/assets/images/emoji/crocodile.png
deleted file mode 100644
index 3005c46f176..00000000000
--- a/app/assets/images/emoji/crocodile.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/croissant.png b/app/assets/images/emoji/croissant.png
deleted file mode 100644
index fb33feb1a38..00000000000
--- a/app/assets/images/emoji/croissant.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cross.png b/app/assets/images/emoji/cross.png
deleted file mode 100644
index 42b10e82257..00000000000
--- a/app/assets/images/emoji/cross.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crossed_flags.png b/app/assets/images/emoji/crossed_flags.png
deleted file mode 100644
index 273bd0f0fe5..00000000000
--- a/app/assets/images/emoji/crossed_flags.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crossed_swords.png b/app/assets/images/emoji/crossed_swords.png
deleted file mode 100644
index 907e9607134..00000000000
--- a/app/assets/images/emoji/crossed_swords.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crown.png b/app/assets/images/emoji/crown.png
deleted file mode 100644
index 93b82d92f04..00000000000
--- a/app/assets/images/emoji/crown.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cruise_ship.png b/app/assets/images/emoji/cruise_ship.png
deleted file mode 100644
index 19d4acbe40c..00000000000
--- a/app/assets/images/emoji/cruise_ship.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cry.png b/app/assets/images/emoji/cry.png
deleted file mode 100644
index b7877f8a173..00000000000
--- a/app/assets/images/emoji/cry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crying_cat_face.png b/app/assets/images/emoji/crying_cat_face.png
deleted file mode 100644
index b4f49715e00..00000000000
--- a/app/assets/images/emoji/crying_cat_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crystal_ball.png b/app/assets/images/emoji/crystal_ball.png
deleted file mode 100644
index 485d5c888f1..00000000000
--- a/app/assets/images/emoji/crystal_ball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cucumber.png b/app/assets/images/emoji/cucumber.png
deleted file mode 100644
index 500807059d2..00000000000
--- a/app/assets/images/emoji/cucumber.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cupid.png b/app/assets/images/emoji/cupid.png
deleted file mode 100644
index 2df0078ddd1..00000000000
--- a/app/assets/images/emoji/cupid.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/curly_loop.png b/app/assets/images/emoji/curly_loop.png
deleted file mode 100644
index 440aa56d50e..00000000000
--- a/app/assets/images/emoji/curly_loop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/currency_exchange.png b/app/assets/images/emoji/currency_exchange.png
deleted file mode 100644
index 4d46c6050e7..00000000000
--- a/app/assets/images/emoji/currency_exchange.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/curry.png b/app/assets/images/emoji/curry.png
deleted file mode 100644
index 69657ca8103..00000000000
--- a/app/assets/images/emoji/curry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/custard.png b/app/assets/images/emoji/custard.png
deleted file mode 100644
index fa3df67b8f6..00000000000
--- a/app/assets/images/emoji/custard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/customs.png b/app/assets/images/emoji/customs.png
deleted file mode 100644
index 21b7ce2c69e..00000000000
--- a/app/assets/images/emoji/customs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cyclone.png b/app/assets/images/emoji/cyclone.png
deleted file mode 100644
index ff00b1afe70..00000000000
--- a/app/assets/images/emoji/cyclone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dagger.png b/app/assets/images/emoji/dagger.png
deleted file mode 100644
index 66e97b0aa25..00000000000
--- a/app/assets/images/emoji/dagger.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancer.png b/app/assets/images/emoji/dancer.png
deleted file mode 100644
index 04b166991cb..00000000000
--- a/app/assets/images/emoji/dancer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone1.png b/app/assets/images/emoji/dancer_tone1.png
deleted file mode 100644
index 2c7b11c3a6e..00000000000
--- a/app/assets/images/emoji/dancer_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone2.png b/app/assets/images/emoji/dancer_tone2.png
deleted file mode 100644
index cb04b1f907e..00000000000
--- a/app/assets/images/emoji/dancer_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone3.png b/app/assets/images/emoji/dancer_tone3.png
deleted file mode 100644
index 98c5bca7b64..00000000000
--- a/app/assets/images/emoji/dancer_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone4.png b/app/assets/images/emoji/dancer_tone4.png
deleted file mode 100644
index fdb1e00cbba..00000000000
--- a/app/assets/images/emoji/dancer_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone5.png b/app/assets/images/emoji/dancer_tone5.png
deleted file mode 100644
index 0e34e0e23f0..00000000000
--- a/app/assets/images/emoji/dancer_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancers.png b/app/assets/images/emoji/dancers.png
deleted file mode 100644
index 67e6ffacb76..00000000000
--- a/app/assets/images/emoji/dancers.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dango.png b/app/assets/images/emoji/dango.png
deleted file mode 100644
index f73f37b01c7..00000000000
--- a/app/assets/images/emoji/dango.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dark_sunglasses.png b/app/assets/images/emoji/dark_sunglasses.png
deleted file mode 100644
index b1b6db0acff..00000000000
--- a/app/assets/images/emoji/dark_sunglasses.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dart.png b/app/assets/images/emoji/dart.png
deleted file mode 100644
index f6704aeb8ba..00000000000
--- a/app/assets/images/emoji/dart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dash.png b/app/assets/images/emoji/dash.png
deleted file mode 100644
index 064b8525c12..00000000000
--- a/app/assets/images/emoji/dash.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/date.png b/app/assets/images/emoji/date.png
deleted file mode 100644
index f05b3da97b8..00000000000
--- a/app/assets/images/emoji/date.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/deciduous_tree.png b/app/assets/images/emoji/deciduous_tree.png
deleted file mode 100644
index 785fc1c30ea..00000000000
--- a/app/assets/images/emoji/deciduous_tree.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/deer.png b/app/assets/images/emoji/deer.png
deleted file mode 100644
index d8698195ff0..00000000000
--- a/app/assets/images/emoji/deer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/department_store.png b/app/assets/images/emoji/department_store.png
deleted file mode 100644
index 58867c7a6e1..00000000000
--- a/app/assets/images/emoji/department_store.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/desert.png b/app/assets/images/emoji/desert.png
deleted file mode 100644
index e9966ff8c65..00000000000
--- a/app/assets/images/emoji/desert.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/desktop.png b/app/assets/images/emoji/desktop.png
deleted file mode 100644
index 909bd42b5e1..00000000000
--- a/app/assets/images/emoji/desktop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png b/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png
deleted file mode 100644
index 2a22a26d1e2..00000000000
--- a/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/diamonds.png b/app/assets/images/emoji/diamonds.png
deleted file mode 100644
index 1f25f51f97a..00000000000
--- a/app/assets/images/emoji/diamonds.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/disappointed.png b/app/assets/images/emoji/disappointed.png
deleted file mode 100644
index efe4e67e23c..00000000000
--- a/app/assets/images/emoji/disappointed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/disappointed_relieved.png b/app/assets/images/emoji/disappointed_relieved.png
deleted file mode 100644
index aef864d2b3d..00000000000
--- a/app/assets/images/emoji/disappointed_relieved.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dividers.png b/app/assets/images/emoji/dividers.png
deleted file mode 100644
index 46a7e403f9d..00000000000
--- a/app/assets/images/emoji/dividers.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dizzy.png b/app/assets/images/emoji/dizzy.png
deleted file mode 100644
index 85f52efad24..00000000000
--- a/app/assets/images/emoji/dizzy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dizzy_face.png b/app/assets/images/emoji/dizzy_face.png
deleted file mode 100644
index 3120316ab5e..00000000000
--- a/app/assets/images/emoji/dizzy_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/do_not_litter.png b/app/assets/images/emoji/do_not_litter.png
deleted file mode 100644
index 341d2575f4f..00000000000
--- a/app/assets/images/emoji/do_not_litter.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dog.png b/app/assets/images/emoji/dog.png
deleted file mode 100644
index 281b81d58bd..00000000000
--- a/app/assets/images/emoji/dog.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dog2.png b/app/assets/images/emoji/dog2.png
deleted file mode 100644
index 976143dbdbe..00000000000
--- a/app/assets/images/emoji/dog2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dollar.png b/app/assets/images/emoji/dollar.png
deleted file mode 100644
index a9904c28293..00000000000
--- a/app/assets/images/emoji/dollar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dolls.png b/app/assets/images/emoji/dolls.png
deleted file mode 100644
index 10955615110..00000000000
--- a/app/assets/images/emoji/dolls.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dolphin.png b/app/assets/images/emoji/dolphin.png
deleted file mode 100644
index 81434809003..00000000000
--- a/app/assets/images/emoji/dolphin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/door.png b/app/assets/images/emoji/door.png
deleted file mode 100644
index 36ae3e27494..00000000000
--- a/app/assets/images/emoji/door.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/doughnut.png b/app/assets/images/emoji/doughnut.png
deleted file mode 100644
index 0ca4cd0bde8..00000000000
--- a/app/assets/images/emoji/doughnut.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dove.png b/app/assets/images/emoji/dove.png
deleted file mode 100644
index 9580c4917d7..00000000000
--- a/app/assets/images/emoji/dove.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dragon.png b/app/assets/images/emoji/dragon.png
deleted file mode 100644
index d6311cf5429..00000000000
--- a/app/assets/images/emoji/dragon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dragon_face.png b/app/assets/images/emoji/dragon_face.png
deleted file mode 100644
index 3c2720446c6..00000000000
--- a/app/assets/images/emoji/dragon_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dress.png b/app/assets/images/emoji/dress.png
deleted file mode 100644
index a697ca5c57d..00000000000
--- a/app/assets/images/emoji/dress.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dromedary_camel.png b/app/assets/images/emoji/dromedary_camel.png
deleted file mode 100644
index 5271637c7c4..00000000000
--- a/app/assets/images/emoji/dromedary_camel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/drooling_face.png b/app/assets/images/emoji/drooling_face.png
deleted file mode 100644
index a5460532597..00000000000
--- a/app/assets/images/emoji/drooling_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/droplet.png b/app/assets/images/emoji/droplet.png
deleted file mode 100644
index 71241ec3061..00000000000
--- a/app/assets/images/emoji/droplet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/drum.png b/app/assets/images/emoji/drum.png
deleted file mode 100644
index b038727cc99..00000000000
--- a/app/assets/images/emoji/drum.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/duck.png b/app/assets/images/emoji/duck.png
deleted file mode 100644
index 74330b77ca3..00000000000
--- a/app/assets/images/emoji/duck.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dvd.png b/app/assets/images/emoji/dvd.png
deleted file mode 100644
index 045a6f7a08d..00000000000
--- a/app/assets/images/emoji/dvd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/e-mail.png b/app/assets/images/emoji/e-mail.png
deleted file mode 100644
index d22e654a20b..00000000000
--- a/app/assets/images/emoji/e-mail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eagle.png b/app/assets/images/emoji/eagle.png
deleted file mode 100644
index 4f277debeef..00000000000
--- a/app/assets/images/emoji/eagle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear.png b/app/assets/images/emoji/ear.png
deleted file mode 100644
index f84f9ff154a..00000000000
--- a/app/assets/images/emoji/ear.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear_of_rice.png b/app/assets/images/emoji/ear_of_rice.png
deleted file mode 100644
index 3564d9d643a..00000000000
--- a/app/assets/images/emoji/ear_of_rice.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone1.png b/app/assets/images/emoji/ear_tone1.png
deleted file mode 100644
index d09e1e41996..00000000000
--- a/app/assets/images/emoji/ear_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone2.png b/app/assets/images/emoji/ear_tone2.png
deleted file mode 100644
index 300d60a9948..00000000000
--- a/app/assets/images/emoji/ear_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone3.png b/app/assets/images/emoji/ear_tone3.png
deleted file mode 100644
index 2a56eebe445..00000000000
--- a/app/assets/images/emoji/ear_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone4.png b/app/assets/images/emoji/ear_tone4.png
deleted file mode 100644
index bd270f7763e..00000000000
--- a/app/assets/images/emoji/ear_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone5.png b/app/assets/images/emoji/ear_tone5.png
deleted file mode 100644
index b96bb441dff..00000000000
--- a/app/assets/images/emoji/ear_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/earth_africa.png b/app/assets/images/emoji/earth_africa.png
deleted file mode 100644
index 66c3348c23a..00000000000
--- a/app/assets/images/emoji/earth_africa.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/earth_americas.png b/app/assets/images/emoji/earth_americas.png
deleted file mode 100644
index 538c3cddd68..00000000000
--- a/app/assets/images/emoji/earth_americas.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/earth_asia.png b/app/assets/images/emoji/earth_asia.png
deleted file mode 100644
index d8df97fec3c..00000000000
--- a/app/assets/images/emoji/earth_asia.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/egg.png b/app/assets/images/emoji/egg.png
deleted file mode 100644
index c171974d993..00000000000
--- a/app/assets/images/emoji/egg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eggplant.png b/app/assets/images/emoji/eggplant.png
deleted file mode 100644
index fafd7c1a14c..00000000000
--- a/app/assets/images/emoji/eggplant.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eight.png b/app/assets/images/emoji/eight.png
deleted file mode 100644
index 8c95874d4c5..00000000000
--- a/app/assets/images/emoji/eight.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eight_pointed_black_star.png b/app/assets/images/emoji/eight_pointed_black_star.png
deleted file mode 100644
index 820179bda50..00000000000
--- a/app/assets/images/emoji/eight_pointed_black_star.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eight_spoked_asterisk.png b/app/assets/images/emoji/eight_spoked_asterisk.png
deleted file mode 100644
index 3307ffa62ee..00000000000
--- a/app/assets/images/emoji/eight_spoked_asterisk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eject.png b/app/assets/images/emoji/eject.png
deleted file mode 100644
index ec5cfc48973..00000000000
--- a/app/assets/images/emoji/eject.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/electric_plug.png b/app/assets/images/emoji/electric_plug.png
deleted file mode 100644
index 31d1eb215b4..00000000000
--- a/app/assets/images/emoji/electric_plug.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/elephant.png b/app/assets/images/emoji/elephant.png
deleted file mode 100644
index b8a6d140595..00000000000
--- a/app/assets/images/emoji/elephant.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/end.png b/app/assets/images/emoji/end.png
deleted file mode 100644
index ef3ccd5f367..00000000000
--- a/app/assets/images/emoji/end.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/envelope.png b/app/assets/images/emoji/envelope.png
deleted file mode 100644
index ec77ac375a4..00000000000
--- a/app/assets/images/emoji/envelope.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/envelope_with_arrow.png b/app/assets/images/emoji/envelope_with_arrow.png
deleted file mode 100644
index 7448a6b7673..00000000000
--- a/app/assets/images/emoji/envelope_with_arrow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/euro.png b/app/assets/images/emoji/euro.png
deleted file mode 100644
index a49020820e1..00000000000
--- a/app/assets/images/emoji/euro.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/european_castle.png b/app/assets/images/emoji/european_castle.png
deleted file mode 100644
index 888d11332ce..00000000000
--- a/app/assets/images/emoji/european_castle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/european_post_office.png b/app/assets/images/emoji/european_post_office.png
deleted file mode 100644
index 3745aff8dd2..00000000000
--- a/app/assets/images/emoji/european_post_office.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/evergreen_tree.png b/app/assets/images/emoji/evergreen_tree.png
deleted file mode 100644
index f679d8dd772..00000000000
--- a/app/assets/images/emoji/evergreen_tree.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/exclamation.png b/app/assets/images/emoji/exclamation.png
deleted file mode 100644
index 2c14406422f..00000000000
--- a/app/assets/images/emoji/exclamation.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/expressionless.png b/app/assets/images/emoji/expressionless.png
deleted file mode 100644
index 2954017f6c2..00000000000
--- a/app/assets/images/emoji/expressionless.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eye.png b/app/assets/images/emoji/eye.png
deleted file mode 100644
index 9d989cdd375..00000000000
--- a/app/assets/images/emoji/eye.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eye_in_speech_bubble.png b/app/assets/images/emoji/eye_in_speech_bubble.png
deleted file mode 100644
index 21bd22bbcce..00000000000
--- a/app/assets/images/emoji/eye_in_speech_bubble.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eyeglasses.png b/app/assets/images/emoji/eyeglasses.png
deleted file mode 100644
index 865d8274acf..00000000000
--- a/app/assets/images/emoji/eyeglasses.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eyes.png b/app/assets/images/emoji/eyes.png
deleted file mode 100644
index 2102ada7e09..00000000000
--- a/app/assets/images/emoji/eyes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/face_palm.png b/app/assets/images/emoji/face_palm.png
deleted file mode 100644
index defc796cf16..00000000000
--- a/app/assets/images/emoji/face_palm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone1.png b/app/assets/images/emoji/face_palm_tone1.png
deleted file mode 100644
index 2f4b010bb40..00000000000
--- a/app/assets/images/emoji/face_palm_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone2.png b/app/assets/images/emoji/face_palm_tone2.png
deleted file mode 100644
index 97fb6831687..00000000000
--- a/app/assets/images/emoji/face_palm_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone3.png b/app/assets/images/emoji/face_palm_tone3.png
deleted file mode 100644
index b5b5c1e5306..00000000000
--- a/app/assets/images/emoji/face_palm_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone4.png b/app/assets/images/emoji/face_palm_tone4.png
deleted file mode 100644
index 2840b113483..00000000000
--- a/app/assets/images/emoji/face_palm_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone5.png b/app/assets/images/emoji/face_palm_tone5.png
deleted file mode 100644
index 6f070db98be..00000000000
--- a/app/assets/images/emoji/face_palm_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/factory.png b/app/assets/images/emoji/factory.png
deleted file mode 100644
index e1d2ddf4a27..00000000000
--- a/app/assets/images/emoji/factory.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fallen_leaf.png b/app/assets/images/emoji/fallen_leaf.png
deleted file mode 100644
index 0d60e7bdf2d..00000000000
--- a/app/assets/images/emoji/fallen_leaf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family.png b/app/assets/images/emoji/family.png
deleted file mode 100644
index 26421965791..00000000000
--- a/app/assets/images/emoji/family.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mmb.png b/app/assets/images/emoji/family_mmb.png
deleted file mode 100644
index 7a2e4e2c491..00000000000
--- a/app/assets/images/emoji/family_mmb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mmbb.png b/app/assets/images/emoji/family_mmbb.png
deleted file mode 100644
index 81e6c0fc0ee..00000000000
--- a/app/assets/images/emoji/family_mmbb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mmg.png b/app/assets/images/emoji/family_mmg.png
deleted file mode 100644
index 932a85e1fe5..00000000000
--- a/app/assets/images/emoji/family_mmg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mmgb.png b/app/assets/images/emoji/family_mmgb.png
deleted file mode 100644
index 41e35166670..00000000000
--- a/app/assets/images/emoji/family_mmgb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mmgg.png b/app/assets/images/emoji/family_mmgg.png
deleted file mode 100644
index 8e8ccfe6c7f..00000000000
--- a/app/assets/images/emoji/family_mmgg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mwbb.png b/app/assets/images/emoji/family_mwbb.png
deleted file mode 100644
index b544fbe573f..00000000000
--- a/app/assets/images/emoji/family_mwbb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mwg.png b/app/assets/images/emoji/family_mwg.png
deleted file mode 100644
index 71d2681c32a..00000000000
--- a/app/assets/images/emoji/family_mwg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mwgb.png b/app/assets/images/emoji/family_mwgb.png
deleted file mode 100644
index 40dbf1f7a18..00000000000
--- a/app/assets/images/emoji/family_mwgb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mwgg.png b/app/assets/images/emoji/family_mwgg.png
deleted file mode 100644
index bfefa4879cb..00000000000
--- a/app/assets/images/emoji/family_mwgg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_wwb.png b/app/assets/images/emoji/family_wwb.png
deleted file mode 100644
index 836feae7c78..00000000000
--- a/app/assets/images/emoji/family_wwb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_wwbb.png b/app/assets/images/emoji/family_wwbb.png
deleted file mode 100644
index 6c6ba45e7bb..00000000000
--- a/app/assets/images/emoji/family_wwbb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_wwg.png b/app/assets/images/emoji/family_wwg.png
deleted file mode 100644
index 41225c6fa5a..00000000000
--- a/app/assets/images/emoji/family_wwg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_wwgb.png b/app/assets/images/emoji/family_wwgb.png
deleted file mode 100644
index 284d29ab5da..00000000000
--- a/app/assets/images/emoji/family_wwgb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_wwgg.png b/app/assets/images/emoji/family_wwgg.png
deleted file mode 100644
index d8d3f49b85f..00000000000
--- a/app/assets/images/emoji/family_wwgg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fast_forward.png b/app/assets/images/emoji/fast_forward.png
deleted file mode 100644
index c406fedfdb1..00000000000
--- a/app/assets/images/emoji/fast_forward.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fax.png b/app/assets/images/emoji/fax.png
deleted file mode 100644
index 6f929e294c2..00000000000
--- a/app/assets/images/emoji/fax.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fearful.png b/app/assets/images/emoji/fearful.png
deleted file mode 100644
index eb8b347cef9..00000000000
--- a/app/assets/images/emoji/fearful.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/feet.png b/app/assets/images/emoji/feet.png
deleted file mode 100644
index 5fe568cee93..00000000000
--- a/app/assets/images/emoji/feet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fencer.png b/app/assets/images/emoji/fencer.png
deleted file mode 100644
index 5288c920eb9..00000000000
--- a/app/assets/images/emoji/fencer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ferris_wheel.png b/app/assets/images/emoji/ferris_wheel.png
deleted file mode 100644
index 55c8ff0475b..00000000000
--- a/app/assets/images/emoji/ferris_wheel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ferry.png b/app/assets/images/emoji/ferry.png
deleted file mode 100644
index 41816b3ae34..00000000000
--- a/app/assets/images/emoji/ferry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/field_hockey.png b/app/assets/images/emoji/field_hockey.png
deleted file mode 100644
index 839637716ee..00000000000
--- a/app/assets/images/emoji/field_hockey.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/file_cabinet.png b/app/assets/images/emoji/file_cabinet.png
deleted file mode 100644
index fddc65dde96..00000000000
--- a/app/assets/images/emoji/file_cabinet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/file_folder.png b/app/assets/images/emoji/file_folder.png
deleted file mode 100644
index addedaf0870..00000000000
--- a/app/assets/images/emoji/file_folder.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/film_frames.png b/app/assets/images/emoji/film_frames.png
deleted file mode 100644
index 30143aedbe6..00000000000
--- a/app/assets/images/emoji/film_frames.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed.png b/app/assets/images/emoji/fingers_crossed.png
deleted file mode 100644
index 4cd18514ea3..00000000000
--- a/app/assets/images/emoji/fingers_crossed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone1.png b/app/assets/images/emoji/fingers_crossed_tone1.png
deleted file mode 100644
index dd2384a6cd5..00000000000
--- a/app/assets/images/emoji/fingers_crossed_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone2.png b/app/assets/images/emoji/fingers_crossed_tone2.png
deleted file mode 100644
index 6228401befe..00000000000
--- a/app/assets/images/emoji/fingers_crossed_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone3.png b/app/assets/images/emoji/fingers_crossed_tone3.png
deleted file mode 100644
index b1074da15f5..00000000000
--- a/app/assets/images/emoji/fingers_crossed_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone4.png b/app/assets/images/emoji/fingers_crossed_tone4.png
deleted file mode 100644
index 75e05e4d332..00000000000
--- a/app/assets/images/emoji/fingers_crossed_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone5.png b/app/assets/images/emoji/fingers_crossed_tone5.png
deleted file mode 100644
index 761aebdc30f..00000000000
--- a/app/assets/images/emoji/fingers_crossed_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fire.png b/app/assets/images/emoji/fire.png
deleted file mode 100644
index bd3775a460b..00000000000
--- a/app/assets/images/emoji/fire.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fire_engine.png b/app/assets/images/emoji/fire_engine.png
deleted file mode 100644
index 2cd45b7cf7e..00000000000
--- a/app/assets/images/emoji/fire_engine.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fireworks.png b/app/assets/images/emoji/fireworks.png
deleted file mode 100644
index 176c8b58265..00000000000
--- a/app/assets/images/emoji/fireworks.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/first_place.png b/app/assets/images/emoji/first_place.png
deleted file mode 100644
index 15612b66492..00000000000
--- a/app/assets/images/emoji/first_place.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/first_quarter_moon.png b/app/assets/images/emoji/first_quarter_moon.png
deleted file mode 100644
index 5dccaf72a4f..00000000000
--- a/app/assets/images/emoji/first_quarter_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/first_quarter_moon_with_face.png b/app/assets/images/emoji/first_quarter_moon_with_face.png
deleted file mode 100644
index cd8a3d7acd8..00000000000
--- a/app/assets/images/emoji/first_quarter_moon_with_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fish.png b/app/assets/images/emoji/fish.png
deleted file mode 100644
index c2d2faaacd4..00000000000
--- a/app/assets/images/emoji/fish.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fish_cake.png b/app/assets/images/emoji/fish_cake.png
deleted file mode 100644
index 157bded65db..00000000000
--- a/app/assets/images/emoji/fish_cake.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fishing_pole_and_fish.png b/app/assets/images/emoji/fishing_pole_and_fish.png
deleted file mode 100644
index dfcdf07eb50..00000000000
--- a/app/assets/images/emoji/fishing_pole_and_fish.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fist.png b/app/assets/images/emoji/fist.png
deleted file mode 100644
index de33592bf98..00000000000
--- a/app/assets/images/emoji/fist.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone1.png b/app/assets/images/emoji/fist_tone1.png
deleted file mode 100644
index 02809e2dd68..00000000000
--- a/app/assets/images/emoji/fist_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone2.png b/app/assets/images/emoji/fist_tone2.png
deleted file mode 100644
index 5de34810383..00000000000
--- a/app/assets/images/emoji/fist_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone3.png b/app/assets/images/emoji/fist_tone3.png
deleted file mode 100644
index 0d5240129b1..00000000000
--- a/app/assets/images/emoji/fist_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone4.png b/app/assets/images/emoji/fist_tone4.png
deleted file mode 100644
index a95c0dd634b..00000000000
--- a/app/assets/images/emoji/fist_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone5.png b/app/assets/images/emoji/fist_tone5.png
deleted file mode 100644
index a2f092fd8c7..00000000000
--- a/app/assets/images/emoji/fist_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/five.png b/app/assets/images/emoji/five.png
deleted file mode 100644
index d14371f3f27..00000000000
--- a/app/assets/images/emoji/five.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ac.png b/app/assets/images/emoji/flag_ac.png
deleted file mode 100644
index 286239920c7..00000000000
--- a/app/assets/images/emoji/flag_ac.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ad.png b/app/assets/images/emoji/flag_ad.png
deleted file mode 100644
index 20f4b14e8ad..00000000000
--- a/app/assets/images/emoji/flag_ad.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ae.png b/app/assets/images/emoji/flag_ae.png
deleted file mode 100644
index d16ffe4b862..00000000000
--- a/app/assets/images/emoji/flag_ae.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_af.png b/app/assets/images/emoji/flag_af.png
deleted file mode 100644
index a51533b554d..00000000000
--- a/app/assets/images/emoji/flag_af.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ag.png b/app/assets/images/emoji/flag_ag.png
deleted file mode 100644
index 07f2ce397d0..00000000000
--- a/app/assets/images/emoji/flag_ag.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ai.png b/app/assets/images/emoji/flag_ai.png
deleted file mode 100644
index 500b5ab09fb..00000000000
--- a/app/assets/images/emoji/flag_ai.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_al.png b/app/assets/images/emoji/flag_al.png
deleted file mode 100644
index 03a20132cc6..00000000000
--- a/app/assets/images/emoji/flag_al.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_am.png b/app/assets/images/emoji/flag_am.png
deleted file mode 100644
index 2ad60a273ec..00000000000
--- a/app/assets/images/emoji/flag_am.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ao.png b/app/assets/images/emoji/flag_ao.png
deleted file mode 100644
index cb46c31f862..00000000000
--- a/app/assets/images/emoji/flag_ao.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_aq.png b/app/assets/images/emoji/flag_aq.png
deleted file mode 100644
index b272021d375..00000000000
--- a/app/assets/images/emoji/flag_aq.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ar.png b/app/assets/images/emoji/flag_ar.png
deleted file mode 100644
index 73136caf3b7..00000000000
--- a/app/assets/images/emoji/flag_ar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_as.png b/app/assets/images/emoji/flag_as.png
deleted file mode 100644
index 3db45a0d9f3..00000000000
--- a/app/assets/images/emoji/flag_as.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_at.png b/app/assets/images/emoji/flag_at.png
deleted file mode 100644
index c43769dcb19..00000000000
--- a/app/assets/images/emoji/flag_at.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_au.png b/app/assets/images/emoji/flag_au.png
deleted file mode 100644
index 7794309c78c..00000000000
--- a/app/assets/images/emoji/flag_au.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_aw.png b/app/assets/images/emoji/flag_aw.png
deleted file mode 100644
index 02c840d12c9..00000000000
--- a/app/assets/images/emoji/flag_aw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ax.png b/app/assets/images/emoji/flag_ax.png
deleted file mode 100644
index fc5466174bb..00000000000
--- a/app/assets/images/emoji/flag_ax.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_az.png b/app/assets/images/emoji/flag_az.png
deleted file mode 100644
index 89d3d15fd9f..00000000000
--- a/app/assets/images/emoji/flag_az.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ba.png b/app/assets/images/emoji/flag_ba.png
deleted file mode 100644
index 25fe407e13c..00000000000
--- a/app/assets/images/emoji/flag_ba.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bb.png b/app/assets/images/emoji/flag_bb.png
deleted file mode 100644
index bccd8c5c9b0..00000000000
--- a/app/assets/images/emoji/flag_bb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bd.png b/app/assets/images/emoji/flag_bd.png
deleted file mode 100644
index b0597a3149b..00000000000
--- a/app/assets/images/emoji/flag_bd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_be.png b/app/assets/images/emoji/flag_be.png
deleted file mode 100644
index 551f086e3c4..00000000000
--- a/app/assets/images/emoji/flag_be.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bf.png b/app/assets/images/emoji/flag_bf.png
deleted file mode 100644
index 444d4829f94..00000000000
--- a/app/assets/images/emoji/flag_bf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bg.png b/app/assets/images/emoji/flag_bg.png
deleted file mode 100644
index 821eee5e170..00000000000
--- a/app/assets/images/emoji/flag_bg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bh.png b/app/assets/images/emoji/flag_bh.png
deleted file mode 100644
index f33724249f0..00000000000
--- a/app/assets/images/emoji/flag_bh.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bi.png b/app/assets/images/emoji/flag_bi.png
deleted file mode 100644
index ea20ac93211..00000000000
--- a/app/assets/images/emoji/flag_bi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bj.png b/app/assets/images/emoji/flag_bj.png
deleted file mode 100644
index 7cca4f80457..00000000000
--- a/app/assets/images/emoji/flag_bj.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bl.png b/app/assets/images/emoji/flag_bl.png
deleted file mode 100644
index 1082e78999f..00000000000
--- a/app/assets/images/emoji/flag_bl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_black.png b/app/assets/images/emoji/flag_black.png
deleted file mode 100644
index 0e28d05d5ac..00000000000
--- a/app/assets/images/emoji/flag_black.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bm.png b/app/assets/images/emoji/flag_bm.png
deleted file mode 100644
index ab8cafdac63..00000000000
--- a/app/assets/images/emoji/flag_bm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bn.png b/app/assets/images/emoji/flag_bn.png
deleted file mode 100644
index caa9329a896..00000000000
--- a/app/assets/images/emoji/flag_bn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bo.png b/app/assets/images/emoji/flag_bo.png
deleted file mode 100644
index 98af62b3da7..00000000000
--- a/app/assets/images/emoji/flag_bo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bq.png b/app/assets/images/emoji/flag_bq.png
deleted file mode 100644
index cb978ef9de9..00000000000
--- a/app/assets/images/emoji/flag_bq.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_br.png b/app/assets/images/emoji/flag_br.png
deleted file mode 100644
index b139366a42b..00000000000
--- a/app/assets/images/emoji/flag_br.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bs.png b/app/assets/images/emoji/flag_bs.png
deleted file mode 100644
index d36bcd2fb52..00000000000
--- a/app/assets/images/emoji/flag_bs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bt.png b/app/assets/images/emoji/flag_bt.png
deleted file mode 100644
index ed57aa0360e..00000000000
--- a/app/assets/images/emoji/flag_bt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bv.png b/app/assets/images/emoji/flag_bv.png
deleted file mode 100644
index 5884e648228..00000000000
--- a/app/assets/images/emoji/flag_bv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bw.png b/app/assets/images/emoji/flag_bw.png
deleted file mode 100644
index cb12f34739d..00000000000
--- a/app/assets/images/emoji/flag_bw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_by.png b/app/assets/images/emoji/flag_by.png
deleted file mode 100644
index 859c05beb13..00000000000
--- a/app/assets/images/emoji/flag_by.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bz.png b/app/assets/images/emoji/flag_bz.png
deleted file mode 100644
index 34761cd03d8..00000000000
--- a/app/assets/images/emoji/flag_bz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ca.png b/app/assets/images/emoji/flag_ca.png
deleted file mode 100644
index 7c5b390e85b..00000000000
--- a/app/assets/images/emoji/flag_ca.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cc.png b/app/assets/images/emoji/flag_cc.png
deleted file mode 100644
index b6555a23d83..00000000000
--- a/app/assets/images/emoji/flag_cc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cd.png b/app/assets/images/emoji/flag_cd.png
deleted file mode 100644
index fa92009771d..00000000000
--- a/app/assets/images/emoji/flag_cd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cf.png b/app/assets/images/emoji/flag_cf.png
deleted file mode 100644
index b969ae29ea9..00000000000
--- a/app/assets/images/emoji/flag_cf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cg.png b/app/assets/images/emoji/flag_cg.png
deleted file mode 100644
index 3a38a40a95e..00000000000
--- a/app/assets/images/emoji/flag_cg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ch.png b/app/assets/images/emoji/flag_ch.png
deleted file mode 100644
index 5ff86b8a3b7..00000000000
--- a/app/assets/images/emoji/flag_ch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ci.png b/app/assets/images/emoji/flag_ci.png
deleted file mode 100644
index e3b4d15c7f1..00000000000
--- a/app/assets/images/emoji/flag_ci.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ck.png b/app/assets/images/emoji/flag_ck.png
deleted file mode 100644
index b6b53dbc1c4..00000000000
--- a/app/assets/images/emoji/flag_ck.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cl.png b/app/assets/images/emoji/flag_cl.png
deleted file mode 100644
index c9390da5499..00000000000
--- a/app/assets/images/emoji/flag_cl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cm.png b/app/assets/images/emoji/flag_cm.png
deleted file mode 100644
index 2d3f6ec4518..00000000000
--- a/app/assets/images/emoji/flag_cm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cn.png b/app/assets/images/emoji/flag_cn.png
deleted file mode 100644
index 0a7f350a6d2..00000000000
--- a/app/assets/images/emoji/flag_cn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_co.png b/app/assets/images/emoji/flag_co.png
deleted file mode 100644
index 7e0f5e0dc3c..00000000000
--- a/app/assets/images/emoji/flag_co.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cp.png b/app/assets/images/emoji/flag_cp.png
deleted file mode 100644
index 70c761036bd..00000000000
--- a/app/assets/images/emoji/flag_cp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cr.png b/app/assets/images/emoji/flag_cr.png
deleted file mode 100644
index a5fce126515..00000000000
--- a/app/assets/images/emoji/flag_cr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cu.png b/app/assets/images/emoji/flag_cu.png
deleted file mode 100644
index 447328f7dfd..00000000000
--- a/app/assets/images/emoji/flag_cu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cv.png b/app/assets/images/emoji/flag_cv.png
deleted file mode 100644
index 43faf4d64d5..00000000000
--- a/app/assets/images/emoji/flag_cv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cw.png b/app/assets/images/emoji/flag_cw.png
deleted file mode 100644
index eb39e8d0078..00000000000
--- a/app/assets/images/emoji/flag_cw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cx.png b/app/assets/images/emoji/flag_cx.png
deleted file mode 100644
index 09d21359f3a..00000000000
--- a/app/assets/images/emoji/flag_cx.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cy.png b/app/assets/images/emoji/flag_cy.png
deleted file mode 100644
index 154a7aa3176..00000000000
--- a/app/assets/images/emoji/flag_cy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cz.png b/app/assets/images/emoji/flag_cz.png
deleted file mode 100644
index 9737ca223c7..00000000000
--- a/app/assets/images/emoji/flag_cz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_de.png b/app/assets/images/emoji/flag_de.png
deleted file mode 100644
index 98ed76b3bab..00000000000
--- a/app/assets/images/emoji/flag_de.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_dg.png b/app/assets/images/emoji/flag_dg.png
deleted file mode 100644
index aae927d14b8..00000000000
--- a/app/assets/images/emoji/flag_dg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_dj.png b/app/assets/images/emoji/flag_dj.png
deleted file mode 100644
index 73c2a2acbd9..00000000000
--- a/app/assets/images/emoji/flag_dj.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_dk.png b/app/assets/images/emoji/flag_dk.png
deleted file mode 100644
index e5a60b06256..00000000000
--- a/app/assets/images/emoji/flag_dk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_dm.png b/app/assets/images/emoji/flag_dm.png
deleted file mode 100644
index 50f8a53981d..00000000000
--- a/app/assets/images/emoji/flag_dm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_do.png b/app/assets/images/emoji/flag_do.png
deleted file mode 100644
index 037a45d7c26..00000000000
--- a/app/assets/images/emoji/flag_do.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_dz.png b/app/assets/images/emoji/flag_dz.png
deleted file mode 100644
index 24945b10f2d..00000000000
--- a/app/assets/images/emoji/flag_dz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ea.png b/app/assets/images/emoji/flag_ea.png
deleted file mode 100644
index 356ff347838..00000000000
--- a/app/assets/images/emoji/flag_ea.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ec.png b/app/assets/images/emoji/flag_ec.png
deleted file mode 100644
index 13814594619..00000000000
--- a/app/assets/images/emoji/flag_ec.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ee.png b/app/assets/images/emoji/flag_ee.png
deleted file mode 100644
index 84f317e7747..00000000000
--- a/app/assets/images/emoji/flag_ee.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_eg.png b/app/assets/images/emoji/flag_eg.png
deleted file mode 100644
index 57786064a95..00000000000
--- a/app/assets/images/emoji/flag_eg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_eh.png b/app/assets/images/emoji/flag_eh.png
deleted file mode 100644
index 4d7a76687f6..00000000000
--- a/app/assets/images/emoji/flag_eh.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_er.png b/app/assets/images/emoji/flag_er.png
deleted file mode 100644
index 0c3c724c1fb..00000000000
--- a/app/assets/images/emoji/flag_er.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_es.png b/app/assets/images/emoji/flag_es.png
deleted file mode 100644
index 3e73597a225..00000000000
--- a/app/assets/images/emoji/flag_es.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_et.png b/app/assets/images/emoji/flag_et.png
deleted file mode 100644
index 9560a134c97..00000000000
--- a/app/assets/images/emoji/flag_et.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_eu.png b/app/assets/images/emoji/flag_eu.png
deleted file mode 100644
index 0b456cf3330..00000000000
--- a/app/assets/images/emoji/flag_eu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_fi.png b/app/assets/images/emoji/flag_fi.png
deleted file mode 100644
index ebcf58abfc5..00000000000
--- a/app/assets/images/emoji/flag_fi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_fj.png b/app/assets/images/emoji/flag_fj.png
deleted file mode 100644
index 9cc8c37fe37..00000000000
--- a/app/assets/images/emoji/flag_fj.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_fk.png b/app/assets/images/emoji/flag_fk.png
deleted file mode 100644
index 61372fd2549..00000000000
--- a/app/assets/images/emoji/flag_fk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_fm.png b/app/assets/images/emoji/flag_fm.png
deleted file mode 100644
index 0889825c8e1..00000000000
--- a/app/assets/images/emoji/flag_fm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_fo.png b/app/assets/images/emoji/flag_fo.png
deleted file mode 100644
index 9a4431b0831..00000000000
--- a/app/assets/images/emoji/flag_fo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_fr.png b/app/assets/images/emoji/flag_fr.png
deleted file mode 100644
index 62ca19c3fcf..00000000000
--- a/app/assets/images/emoji/flag_fr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ga.png b/app/assets/images/emoji/flag_ga.png
deleted file mode 100644
index 2e68e527a3e..00000000000
--- a/app/assets/images/emoji/flag_ga.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gb.png b/app/assets/images/emoji/flag_gb.png
deleted file mode 100644
index 3ed10f62347..00000000000
--- a/app/assets/images/emoji/flag_gb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gd.png b/app/assets/images/emoji/flag_gd.png
deleted file mode 100644
index 527aad33807..00000000000
--- a/app/assets/images/emoji/flag_gd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ge.png b/app/assets/images/emoji/flag_ge.png
deleted file mode 100644
index a75d142480d..00000000000
--- a/app/assets/images/emoji/flag_ge.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gf.png b/app/assets/images/emoji/flag_gf.png
deleted file mode 100644
index 0cf96f327c0..00000000000
--- a/app/assets/images/emoji/flag_gf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gg.png b/app/assets/images/emoji/flag_gg.png
deleted file mode 100644
index 970002c7f76..00000000000
--- a/app/assets/images/emoji/flag_gg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gh.png b/app/assets/images/emoji/flag_gh.png
deleted file mode 100644
index f31b5eb7b45..00000000000
--- a/app/assets/images/emoji/flag_gh.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gi.png b/app/assets/images/emoji/flag_gi.png
deleted file mode 100644
index e554a2a1d0c..00000000000
--- a/app/assets/images/emoji/flag_gi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gl.png b/app/assets/images/emoji/flag_gl.png
deleted file mode 100644
index 2e795dd4e33..00000000000
--- a/app/assets/images/emoji/flag_gl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gm.png b/app/assets/images/emoji/flag_gm.png
deleted file mode 100644
index bb69c0975a3..00000000000
--- a/app/assets/images/emoji/flag_gm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gn.png b/app/assets/images/emoji/flag_gn.png
deleted file mode 100644
index 1981f61dbf5..00000000000
--- a/app/assets/images/emoji/flag_gn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gp.png b/app/assets/images/emoji/flag_gp.png
deleted file mode 100644
index 10e42e672bd..00000000000
--- a/app/assets/images/emoji/flag_gp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gq.png b/app/assets/images/emoji/flag_gq.png
deleted file mode 100644
index 11475e61eeb..00000000000
--- a/app/assets/images/emoji/flag_gq.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gr.png b/app/assets/images/emoji/flag_gr.png
deleted file mode 100644
index 0f6bb1b6b94..00000000000
--- a/app/assets/images/emoji/flag_gr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gs.png b/app/assets/images/emoji/flag_gs.png
deleted file mode 100644
index 6fc92780453..00000000000
--- a/app/assets/images/emoji/flag_gs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gt.png b/app/assets/images/emoji/flag_gt.png
deleted file mode 100644
index 7213d4139ed..00000000000
--- a/app/assets/images/emoji/flag_gt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gu.png b/app/assets/images/emoji/flag_gu.png
deleted file mode 100644
index 4027549ca3c..00000000000
--- a/app/assets/images/emoji/flag_gu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gw.png b/app/assets/images/emoji/flag_gw.png
deleted file mode 100644
index 6357f6225f4..00000000000
--- a/app/assets/images/emoji/flag_gw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gy.png b/app/assets/images/emoji/flag_gy.png
deleted file mode 100644
index 746e2fb7e44..00000000000
--- a/app/assets/images/emoji/flag_gy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_hk.png b/app/assets/images/emoji/flag_hk.png
deleted file mode 100644
index cf0c7151b56..00000000000
--- a/app/assets/images/emoji/flag_hk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_hm.png b/app/assets/images/emoji/flag_hm.png
deleted file mode 100644
index b613509e466..00000000000
--- a/app/assets/images/emoji/flag_hm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_hn.png b/app/assets/images/emoji/flag_hn.png
deleted file mode 100644
index 402cdcefdf8..00000000000
--- a/app/assets/images/emoji/flag_hn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_hr.png b/app/assets/images/emoji/flag_hr.png
deleted file mode 100644
index 46f4f06b4f2..00000000000
--- a/app/assets/images/emoji/flag_hr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ht.png b/app/assets/images/emoji/flag_ht.png
deleted file mode 100644
index d8d0c888498..00000000000
--- a/app/assets/images/emoji/flag_ht.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_hu.png b/app/assets/images/emoji/flag_hu.png
deleted file mode 100644
index a898de636a5..00000000000
--- a/app/assets/images/emoji/flag_hu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ic.png b/app/assets/images/emoji/flag_ic.png
deleted file mode 100644
index 69fd990aa95..00000000000
--- a/app/assets/images/emoji/flag_ic.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_id.png b/app/assets/images/emoji/flag_id.png
deleted file mode 100644
index 85b4c063a45..00000000000
--- a/app/assets/images/emoji/flag_id.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ie.png b/app/assets/images/emoji/flag_ie.png
deleted file mode 100644
index a28295838cc..00000000000
--- a/app/assets/images/emoji/flag_ie.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_il.png b/app/assets/images/emoji/flag_il.png
deleted file mode 100644
index 85c410d45fb..00000000000
--- a/app/assets/images/emoji/flag_il.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_im.png b/app/assets/images/emoji/flag_im.png
deleted file mode 100644
index 60a2458e38e..00000000000
--- a/app/assets/images/emoji/flag_im.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_in.png b/app/assets/images/emoji/flag_in.png
deleted file mode 100644
index feccc8952ce..00000000000
--- a/app/assets/images/emoji/flag_in.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_io.png b/app/assets/images/emoji/flag_io.png
deleted file mode 100644
index aae927d14b8..00000000000
--- a/app/assets/images/emoji/flag_io.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_iq.png b/app/assets/images/emoji/flag_iq.png
deleted file mode 100644
index 41fd1db6f86..00000000000
--- a/app/assets/images/emoji/flag_iq.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ir.png b/app/assets/images/emoji/flag_ir.png
deleted file mode 100644
index ff7aaf62ba6..00000000000
--- a/app/assets/images/emoji/flag_ir.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_is.png b/app/assets/images/emoji/flag_is.png
deleted file mode 100644
index ad8d4131dd2..00000000000
--- a/app/assets/images/emoji/flag_is.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_it.png b/app/assets/images/emoji/flag_it.png
deleted file mode 100644
index f21563ec533..00000000000
--- a/app/assets/images/emoji/flag_it.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_je.png b/app/assets/images/emoji/flag_je.png
deleted file mode 100644
index 198a918f6a4..00000000000
--- a/app/assets/images/emoji/flag_je.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_jm.png b/app/assets/images/emoji/flag_jm.png
deleted file mode 100644
index f84e4f9e8db..00000000000
--- a/app/assets/images/emoji/flag_jm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_jo.png b/app/assets/images/emoji/flag_jo.png
deleted file mode 100644
index 20bfa147e3e..00000000000
--- a/app/assets/images/emoji/flag_jo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_jp.png b/app/assets/images/emoji/flag_jp.png
deleted file mode 100644
index 8d8838e4708..00000000000
--- a/app/assets/images/emoji/flag_jp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ke.png b/app/assets/images/emoji/flag_ke.png
deleted file mode 100644
index 9e417ab3009..00000000000
--- a/app/assets/images/emoji/flag_ke.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kg.png b/app/assets/images/emoji/flag_kg.png
deleted file mode 100644
index 2f2d848fe58..00000000000
--- a/app/assets/images/emoji/flag_kg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kh.png b/app/assets/images/emoji/flag_kh.png
deleted file mode 100644
index 9a2877dd620..00000000000
--- a/app/assets/images/emoji/flag_kh.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ki.png b/app/assets/images/emoji/flag_ki.png
deleted file mode 100644
index 10e507e3245..00000000000
--- a/app/assets/images/emoji/flag_ki.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_km.png b/app/assets/images/emoji/flag_km.png
deleted file mode 100644
index bd5a0588e03..00000000000
--- a/app/assets/images/emoji/flag_km.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kn.png b/app/assets/images/emoji/flag_kn.png
deleted file mode 100644
index 776207c9605..00000000000
--- a/app/assets/images/emoji/flag_kn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kp.png b/app/assets/images/emoji/flag_kp.png
deleted file mode 100644
index 6b3fd89eaaa..00000000000
--- a/app/assets/images/emoji/flag_kp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kr.png b/app/assets/images/emoji/flag_kr.png
deleted file mode 100644
index 833a88116e1..00000000000
--- a/app/assets/images/emoji/flag_kr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kw.png b/app/assets/images/emoji/flag_kw.png
deleted file mode 100644
index 4d19bfa6ca7..00000000000
--- a/app/assets/images/emoji/flag_kw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ky.png b/app/assets/images/emoji/flag_ky.png
deleted file mode 100644
index 40daa4da597..00000000000
--- a/app/assets/images/emoji/flag_ky.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kz.png b/app/assets/images/emoji/flag_kz.png
deleted file mode 100644
index 2f97a8fd3c6..00000000000
--- a/app/assets/images/emoji/flag_kz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_la.png b/app/assets/images/emoji/flag_la.png
deleted file mode 100644
index 4d4179f34f6..00000000000
--- a/app/assets/images/emoji/flag_la.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lb.png b/app/assets/images/emoji/flag_lb.png
deleted file mode 100644
index 3d594467011..00000000000
--- a/app/assets/images/emoji/flag_lb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lc.png b/app/assets/images/emoji/flag_lc.png
deleted file mode 100644
index 45547b1e439..00000000000
--- a/app/assets/images/emoji/flag_lc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_li.png b/app/assets/images/emoji/flag_li.png
deleted file mode 100644
index 0eafa6a2215..00000000000
--- a/app/assets/images/emoji/flag_li.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lk.png b/app/assets/images/emoji/flag_lk.png
deleted file mode 100644
index ab4fe10c40c..00000000000
--- a/app/assets/images/emoji/flag_lk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lr.png b/app/assets/images/emoji/flag_lr.png
deleted file mode 100644
index f66f267fea2..00000000000
--- a/app/assets/images/emoji/flag_lr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ls.png b/app/assets/images/emoji/flag_ls.png
deleted file mode 100644
index 24745631e3c..00000000000
--- a/app/assets/images/emoji/flag_ls.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lt.png b/app/assets/images/emoji/flag_lt.png
deleted file mode 100644
index d644b56d62a..00000000000
--- a/app/assets/images/emoji/flag_lt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lu.png b/app/assets/images/emoji/flag_lu.png
deleted file mode 100644
index a2df9c92994..00000000000
--- a/app/assets/images/emoji/flag_lu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lv.png b/app/assets/images/emoji/flag_lv.png
deleted file mode 100644
index ae680d5f0e3..00000000000
--- a/app/assets/images/emoji/flag_lv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ly.png b/app/assets/images/emoji/flag_ly.png
deleted file mode 100644
index f6e77b0f3ba..00000000000
--- a/app/assets/images/emoji/flag_ly.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ma.png b/app/assets/images/emoji/flag_ma.png
deleted file mode 100644
index c4a056722cd..00000000000
--- a/app/assets/images/emoji/flag_ma.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mc.png b/app/assets/images/emoji/flag_mc.png
deleted file mode 100644
index d479eab98cb..00000000000
--- a/app/assets/images/emoji/flag_mc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_md.png b/app/assets/images/emoji/flag_md.png
deleted file mode 100644
index a7a72539872..00000000000
--- a/app/assets/images/emoji/flag_md.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_me.png b/app/assets/images/emoji/flag_me.png
deleted file mode 100644
index 7c771e7e120..00000000000
--- a/app/assets/images/emoji/flag_me.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mf.png b/app/assets/images/emoji/flag_mf.png
deleted file mode 100644
index 70c761036bd..00000000000
--- a/app/assets/images/emoji/flag_mf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mg.png b/app/assets/images/emoji/flag_mg.png
deleted file mode 100644
index 2f3ccdda76f..00000000000
--- a/app/assets/images/emoji/flag_mg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mh.png b/app/assets/images/emoji/flag_mh.png
deleted file mode 100644
index 598016481c1..00000000000
--- a/app/assets/images/emoji/flag_mh.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mk.png b/app/assets/images/emoji/flag_mk.png
deleted file mode 100644
index 7ba775ee75c..00000000000
--- a/app/assets/images/emoji/flag_mk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ml.png b/app/assets/images/emoji/flag_ml.png
deleted file mode 100644
index 68343785468..00000000000
--- a/app/assets/images/emoji/flag_ml.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mm.png b/app/assets/images/emoji/flag_mm.png
deleted file mode 100644
index 37dc7d71591..00000000000
--- a/app/assets/images/emoji/flag_mm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mn.png b/app/assets/images/emoji/flag_mn.png
deleted file mode 100644
index 1f146bbcd1a..00000000000
--- a/app/assets/images/emoji/flag_mn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mo.png b/app/assets/images/emoji/flag_mo.png
deleted file mode 100644
index 7edde31f64b..00000000000
--- a/app/assets/images/emoji/flag_mo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mp.png b/app/assets/images/emoji/flag_mp.png
deleted file mode 100644
index 17ec1c441ed..00000000000
--- a/app/assets/images/emoji/flag_mp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mq.png b/app/assets/images/emoji/flag_mq.png
deleted file mode 100644
index 1e672dc9087..00000000000
--- a/app/assets/images/emoji/flag_mq.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mr.png b/app/assets/images/emoji/flag_mr.png
deleted file mode 100644
index f87de46effe..00000000000
--- a/app/assets/images/emoji/flag_mr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ms.png b/app/assets/images/emoji/flag_ms.png
deleted file mode 100644
index 480b0d4ebda..00000000000
--- a/app/assets/images/emoji/flag_ms.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mt.png b/app/assets/images/emoji/flag_mt.png
deleted file mode 100644
index c9e1dbdce82..00000000000
--- a/app/assets/images/emoji/flag_mt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mu.png b/app/assets/images/emoji/flag_mu.png
deleted file mode 100644
index 55b33cb7c33..00000000000
--- a/app/assets/images/emoji/flag_mu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mv.png b/app/assets/images/emoji/flag_mv.png
deleted file mode 100644
index ce5867126ae..00000000000
--- a/app/assets/images/emoji/flag_mv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mw.png b/app/assets/images/emoji/flag_mw.png
deleted file mode 100644
index 003d8548401..00000000000
--- a/app/assets/images/emoji/flag_mw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mx.png b/app/assets/images/emoji/flag_mx.png
deleted file mode 100644
index 42572bcd0ba..00000000000
--- a/app/assets/images/emoji/flag_mx.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_my.png b/app/assets/images/emoji/flag_my.png
deleted file mode 100644
index 17526c26742..00000000000
--- a/app/assets/images/emoji/flag_my.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mz.png b/app/assets/images/emoji/flag_mz.png
deleted file mode 100644
index 2352a78e786..00000000000
--- a/app/assets/images/emoji/flag_mz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_na.png b/app/assets/images/emoji/flag_na.png
deleted file mode 100644
index ed31c3df04d..00000000000
--- a/app/assets/images/emoji/flag_na.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_nc.png b/app/assets/images/emoji/flag_nc.png
deleted file mode 100644
index 90b3afebfa3..00000000000
--- a/app/assets/images/emoji/flag_nc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ne.png b/app/assets/images/emoji/flag_ne.png
deleted file mode 100644
index f98a1173c2a..00000000000
--- a/app/assets/images/emoji/flag_ne.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_nf.png b/app/assets/images/emoji/flag_nf.png
deleted file mode 100644
index 9099e767420..00000000000
--- a/app/assets/images/emoji/flag_nf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ng.png b/app/assets/images/emoji/flag_ng.png
deleted file mode 100644
index ea0abeff1a1..00000000000
--- a/app/assets/images/emoji/flag_ng.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ni.png b/app/assets/images/emoji/flag_ni.png
deleted file mode 100644
index 772920dfa10..00000000000
--- a/app/assets/images/emoji/flag_ni.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_nl.png b/app/assets/images/emoji/flag_nl.png
deleted file mode 100644
index 83a0e817e41..00000000000
--- a/app/assets/images/emoji/flag_nl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_no.png b/app/assets/images/emoji/flag_no.png
deleted file mode 100644
index 99d3142eb7b..00000000000
--- a/app/assets/images/emoji/flag_no.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_np.png b/app/assets/images/emoji/flag_np.png
deleted file mode 100644
index 87425a8dfef..00000000000
--- a/app/assets/images/emoji/flag_np.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_nr.png b/app/assets/images/emoji/flag_nr.png
deleted file mode 100644
index b3e3a5d5621..00000000000
--- a/app/assets/images/emoji/flag_nr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_nu.png b/app/assets/images/emoji/flag_nu.png
deleted file mode 100644
index f03614443ee..00000000000
--- a/app/assets/images/emoji/flag_nu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_nz.png b/app/assets/images/emoji/flag_nz.png
deleted file mode 100644
index a4eeeab9cd9..00000000000
--- a/app/assets/images/emoji/flag_nz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_om.png b/app/assets/images/emoji/flag_om.png
deleted file mode 100644
index ea824ba31e7..00000000000
--- a/app/assets/images/emoji/flag_om.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pa.png b/app/assets/images/emoji/flag_pa.png
deleted file mode 100644
index c3091d89889..00000000000
--- a/app/assets/images/emoji/flag_pa.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pe.png b/app/assets/images/emoji/flag_pe.png
deleted file mode 100644
index 39223aa9dbb..00000000000
--- a/app/assets/images/emoji/flag_pe.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pf.png b/app/assets/images/emoji/flag_pf.png
deleted file mode 100644
index 113445f8f6e..00000000000
--- a/app/assets/images/emoji/flag_pf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pg.png b/app/assets/images/emoji/flag_pg.png
deleted file mode 100644
index 825e9dcb762..00000000000
--- a/app/assets/images/emoji/flag_pg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ph.png b/app/assets/images/emoji/flag_ph.png
deleted file mode 100644
index 8260e15bd2c..00000000000
--- a/app/assets/images/emoji/flag_ph.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pk.png b/app/assets/images/emoji/flag_pk.png
deleted file mode 100644
index a7b6a1c5074..00000000000
--- a/app/assets/images/emoji/flag_pk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pl.png b/app/assets/images/emoji/flag_pl.png
deleted file mode 100644
index 19de2edec11..00000000000
--- a/app/assets/images/emoji/flag_pl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pm.png b/app/assets/images/emoji/flag_pm.png
deleted file mode 100644
index 2ca60554193..00000000000
--- a/app/assets/images/emoji/flag_pm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pn.png b/app/assets/images/emoji/flag_pn.png
deleted file mode 100644
index f2263b154bc..00000000000
--- a/app/assets/images/emoji/flag_pn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pr.png b/app/assets/images/emoji/flag_pr.png
deleted file mode 100644
index d0209cddb79..00000000000
--- a/app/assets/images/emoji/flag_pr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ps.png b/app/assets/images/emoji/flag_ps.png
deleted file mode 100644
index 7ccab09778b..00000000000
--- a/app/assets/images/emoji/flag_ps.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pt.png b/app/assets/images/emoji/flag_pt.png
deleted file mode 100644
index cc93f27c64b..00000000000
--- a/app/assets/images/emoji/flag_pt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pw.png b/app/assets/images/emoji/flag_pw.png
deleted file mode 100644
index 154b2f12d3c..00000000000
--- a/app/assets/images/emoji/flag_pw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_py.png b/app/assets/images/emoji/flag_py.png
deleted file mode 100644
index 662ad2f6ff1..00000000000
--- a/app/assets/images/emoji/flag_py.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_qa.png b/app/assets/images/emoji/flag_qa.png
deleted file mode 100644
index a01d8b05cc7..00000000000
--- a/app/assets/images/emoji/flag_qa.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_re.png b/app/assets/images/emoji/flag_re.png
deleted file mode 100644
index 57f2bbe9df8..00000000000
--- a/app/assets/images/emoji/flag_re.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ro.png b/app/assets/images/emoji/flag_ro.png
deleted file mode 100644
index 3e48c447706..00000000000
--- a/app/assets/images/emoji/flag_ro.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_rs.png b/app/assets/images/emoji/flag_rs.png
deleted file mode 100644
index 9df6c9a5235..00000000000
--- a/app/assets/images/emoji/flag_rs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ru.png b/app/assets/images/emoji/flag_ru.png
deleted file mode 100644
index e50c9db90e7..00000000000
--- a/app/assets/images/emoji/flag_ru.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_rw.png b/app/assets/images/emoji/flag_rw.png
deleted file mode 100644
index c238c874e1d..00000000000
--- a/app/assets/images/emoji/flag_rw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sa.png b/app/assets/images/emoji/flag_sa.png
deleted file mode 100644
index 4941be7d198..00000000000
--- a/app/assets/images/emoji/flag_sa.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sb.png b/app/assets/images/emoji/flag_sb.png
deleted file mode 100644
index 7d8f1ac6130..00000000000
--- a/app/assets/images/emoji/flag_sb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sc.png b/app/assets/images/emoji/flag_sc.png
deleted file mode 100644
index 6ae4d90765e..00000000000
--- a/app/assets/images/emoji/flag_sc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sd.png b/app/assets/images/emoji/flag_sd.png
deleted file mode 100644
index 963be1b36fb..00000000000
--- a/app/assets/images/emoji/flag_sd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_se.png b/app/assets/images/emoji/flag_se.png
deleted file mode 100644
index fc0d0e0ce89..00000000000
--- a/app/assets/images/emoji/flag_se.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sg.png b/app/assets/images/emoji/flag_sg.png
deleted file mode 100644
index de3c7737c42..00000000000
--- a/app/assets/images/emoji/flag_sg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sh.png b/app/assets/images/emoji/flag_sh.png
deleted file mode 100644
index 40cd9e44e96..00000000000
--- a/app/assets/images/emoji/flag_sh.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_si.png b/app/assets/images/emoji/flag_si.png
deleted file mode 100644
index e308999dba2..00000000000
--- a/app/assets/images/emoji/flag_si.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sj.png b/app/assets/images/emoji/flag_sj.png
deleted file mode 100644
index 5884e648228..00000000000
--- a/app/assets/images/emoji/flag_sj.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sk.png b/app/assets/images/emoji/flag_sk.png
deleted file mode 100644
index 4259d0e1418..00000000000
--- a/app/assets/images/emoji/flag_sk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sl.png b/app/assets/images/emoji/flag_sl.png
deleted file mode 100644
index d2cc68830ab..00000000000
--- a/app/assets/images/emoji/flag_sl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sm.png b/app/assets/images/emoji/flag_sm.png
deleted file mode 100644
index 03b8708754e..00000000000
--- a/app/assets/images/emoji/flag_sm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sn.png b/app/assets/images/emoji/flag_sn.png
deleted file mode 100644
index 5368bbe93df..00000000000
--- a/app/assets/images/emoji/flag_sn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_so.png b/app/assets/images/emoji/flag_so.png
deleted file mode 100644
index 68a0597365a..00000000000
--- a/app/assets/images/emoji/flag_so.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sr.png b/app/assets/images/emoji/flag_sr.png
deleted file mode 100644
index d3251327035..00000000000
--- a/app/assets/images/emoji/flag_sr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ss.png b/app/assets/images/emoji/flag_ss.png
deleted file mode 100644
index 122977e798f..00000000000
--- a/app/assets/images/emoji/flag_ss.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_st.png b/app/assets/images/emoji/flag_st.png
deleted file mode 100644
index f83a863d612..00000000000
--- a/app/assets/images/emoji/flag_st.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sv.png b/app/assets/images/emoji/flag_sv.png
deleted file mode 100644
index efb83e2f253..00000000000
--- a/app/assets/images/emoji/flag_sv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sx.png b/app/assets/images/emoji/flag_sx.png
deleted file mode 100644
index 94b760fbedf..00000000000
--- a/app/assets/images/emoji/flag_sx.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sy.png b/app/assets/images/emoji/flag_sy.png
deleted file mode 100644
index 09a8ee8f78c..00000000000
--- a/app/assets/images/emoji/flag_sy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sz.png b/app/assets/images/emoji/flag_sz.png
deleted file mode 100644
index f74e82ea1fd..00000000000
--- a/app/assets/images/emoji/flag_sz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ta.png b/app/assets/images/emoji/flag_ta.png
deleted file mode 100644
index b44283e90e2..00000000000
--- a/app/assets/images/emoji/flag_ta.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tc.png b/app/assets/images/emoji/flag_tc.png
deleted file mode 100644
index 156b33d1ba6..00000000000
--- a/app/assets/images/emoji/flag_tc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_td.png b/app/assets/images/emoji/flag_td.png
deleted file mode 100644
index ebe7f592828..00000000000
--- a/app/assets/images/emoji/flag_td.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tf.png b/app/assets/images/emoji/flag_tf.png
deleted file mode 100644
index a1a3ad68ee2..00000000000
--- a/app/assets/images/emoji/flag_tf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tg.png b/app/assets/images/emoji/flag_tg.png
deleted file mode 100644
index 826b73c9ac5..00000000000
--- a/app/assets/images/emoji/flag_tg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_th.png b/app/assets/images/emoji/flag_th.png
deleted file mode 100644
index 93ff542c5a6..00000000000
--- a/app/assets/images/emoji/flag_th.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tj.png b/app/assets/images/emoji/flag_tj.png
deleted file mode 100644
index 7a8a0b6190a..00000000000
--- a/app/assets/images/emoji/flag_tj.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tk.png b/app/assets/images/emoji/flag_tk.png
deleted file mode 100644
index 2fa5a21b1bb..00000000000
--- a/app/assets/images/emoji/flag_tk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tl.png b/app/assets/images/emoji/flag_tl.png
deleted file mode 100644
index 5b120eccc6f..00000000000
--- a/app/assets/images/emoji/flag_tl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tm.png b/app/assets/images/emoji/flag_tm.png
deleted file mode 100644
index c3c4f532302..00000000000
--- a/app/assets/images/emoji/flag_tm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tn.png b/app/assets/images/emoji/flag_tn.png
deleted file mode 100644
index 58ef161229f..00000000000
--- a/app/assets/images/emoji/flag_tn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_to.png b/app/assets/images/emoji/flag_to.png
deleted file mode 100644
index 1ffa7bb9d19..00000000000
--- a/app/assets/images/emoji/flag_to.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tr.png b/app/assets/images/emoji/flag_tr.png
deleted file mode 100644
index 325251fae88..00000000000
--- a/app/assets/images/emoji/flag_tr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tt.png b/app/assets/images/emoji/flag_tt.png
deleted file mode 100644
index ed3bb39a300..00000000000
--- a/app/assets/images/emoji/flag_tt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tv.png b/app/assets/images/emoji/flag_tv.png
deleted file mode 100644
index e82c65c7bb9..00000000000
--- a/app/assets/images/emoji/flag_tv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tw.png b/app/assets/images/emoji/flag_tw.png
deleted file mode 100644
index 3a8f00b5928..00000000000
--- a/app/assets/images/emoji/flag_tw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tz.png b/app/assets/images/emoji/flag_tz.png
deleted file mode 100644
index 2a020853d4e..00000000000
--- a/app/assets/images/emoji/flag_tz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ua.png b/app/assets/images/emoji/flag_ua.png
deleted file mode 100644
index cd84d1bbd36..00000000000
--- a/app/assets/images/emoji/flag_ua.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ug.png b/app/assets/images/emoji/flag_ug.png
deleted file mode 100644
index dc97690eb55..00000000000
--- a/app/assets/images/emoji/flag_ug.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_um.png b/app/assets/images/emoji/flag_um.png
deleted file mode 100644
index 4a7ee3cdf13..00000000000
--- a/app/assets/images/emoji/flag_um.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_us.png b/app/assets/images/emoji/flag_us.png
deleted file mode 100644
index 9f730305860..00000000000
--- a/app/assets/images/emoji/flag_us.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_uy.png b/app/assets/images/emoji/flag_uy.png
deleted file mode 100644
index b8002a697a6..00000000000
--- a/app/assets/images/emoji/flag_uy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_uz.png b/app/assets/images/emoji/flag_uz.png
deleted file mode 100644
index d56ca9bc424..00000000000
--- a/app/assets/images/emoji/flag_uz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_va.png b/app/assets/images/emoji/flag_va.png
deleted file mode 100644
index ddaf5e3141b..00000000000
--- a/app/assets/images/emoji/flag_va.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_vc.png b/app/assets/images/emoji/flag_vc.png
deleted file mode 100644
index 43703c62a71..00000000000
--- a/app/assets/images/emoji/flag_vc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ve.png b/app/assets/images/emoji/flag_ve.png
deleted file mode 100644
index 1b62796824e..00000000000
--- a/app/assets/images/emoji/flag_ve.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_vg.png b/app/assets/images/emoji/flag_vg.png
deleted file mode 100644
index 536f780f1c0..00000000000
--- a/app/assets/images/emoji/flag_vg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_vi.png b/app/assets/images/emoji/flag_vi.png
deleted file mode 100644
index 64102012cfe..00000000000
--- a/app/assets/images/emoji/flag_vi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_vn.png b/app/assets/images/emoji/flag_vn.png
deleted file mode 100644
index 427036046b6..00000000000
--- a/app/assets/images/emoji/flag_vn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_vu.png b/app/assets/images/emoji/flag_vu.png
deleted file mode 100644
index 706eba44070..00000000000
--- a/app/assets/images/emoji/flag_vu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_wf.png b/app/assets/images/emoji/flag_wf.png
deleted file mode 100644
index 70c761036bd..00000000000
--- a/app/assets/images/emoji/flag_wf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_white.png b/app/assets/images/emoji/flag_white.png
deleted file mode 100644
index 86d6e96d5e9..00000000000
--- a/app/assets/images/emoji/flag_white.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ws.png b/app/assets/images/emoji/flag_ws.png
deleted file mode 100644
index a1ea0703141..00000000000
--- a/app/assets/images/emoji/flag_ws.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_xk.png b/app/assets/images/emoji/flag_xk.png
deleted file mode 100644
index e587a446632..00000000000
--- a/app/assets/images/emoji/flag_xk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ye.png b/app/assets/images/emoji/flag_ye.png
deleted file mode 100644
index eadfebd5f67..00000000000
--- a/app/assets/images/emoji/flag_ye.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_yt.png b/app/assets/images/emoji/flag_yt.png
deleted file mode 100644
index c81fa6d886e..00000000000
--- a/app/assets/images/emoji/flag_yt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_za.png b/app/assets/images/emoji/flag_za.png
deleted file mode 100644
index f397ef5072f..00000000000
--- a/app/assets/images/emoji/flag_za.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_zm.png b/app/assets/images/emoji/flag_zm.png
deleted file mode 100644
index 2494a31f662..00000000000
--- a/app/assets/images/emoji/flag_zm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_zw.png b/app/assets/images/emoji/flag_zw.png
deleted file mode 100644
index e09b9652be6..00000000000
--- a/app/assets/images/emoji/flag_zw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flags.png b/app/assets/images/emoji/flags.png
deleted file mode 100644
index 3b451035a3a..00000000000
--- a/app/assets/images/emoji/flags.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flashlight.png b/app/assets/images/emoji/flashlight.png
deleted file mode 100644
index eee36c25067..00000000000
--- a/app/assets/images/emoji/flashlight.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fleur-de-lis.png b/app/assets/images/emoji/fleur-de-lis.png
deleted file mode 100644
index c9250d27fa7..00000000000
--- a/app/assets/images/emoji/fleur-de-lis.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/floppy_disk.png b/app/assets/images/emoji/floppy_disk.png
deleted file mode 100644
index 072a76d3c13..00000000000
--- a/app/assets/images/emoji/floppy_disk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flower_playing_cards.png b/app/assets/images/emoji/flower_playing_cards.png
deleted file mode 100644
index 6766b044d95..00000000000
--- a/app/assets/images/emoji/flower_playing_cards.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flushed.png b/app/assets/images/emoji/flushed.png
deleted file mode 100644
index 829220bc470..00000000000
--- a/app/assets/images/emoji/flushed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fog.png b/app/assets/images/emoji/fog.png
deleted file mode 100644
index 4e73c2de272..00000000000
--- a/app/assets/images/emoji/fog.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/foggy.png b/app/assets/images/emoji/foggy.png
deleted file mode 100644
index 57702d8d3ac..00000000000
--- a/app/assets/images/emoji/foggy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/football.png b/app/assets/images/emoji/football.png
deleted file mode 100644
index 10366f41fce..00000000000
--- a/app/assets/images/emoji/football.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/footprints.png b/app/assets/images/emoji/footprints.png
deleted file mode 100644
index b2673c5a1a8..00000000000
--- a/app/assets/images/emoji/footprints.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fork_and_knife.png b/app/assets/images/emoji/fork_and_knife.png
deleted file mode 100644
index 09f1feaea1c..00000000000
--- a/app/assets/images/emoji/fork_and_knife.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fork_knife_plate.png b/app/assets/images/emoji/fork_knife_plate.png
deleted file mode 100644
index 7411755f708..00000000000
--- a/app/assets/images/emoji/fork_knife_plate.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fountain.png b/app/assets/images/emoji/fountain.png
deleted file mode 100644
index 293f5d91c0f..00000000000
--- a/app/assets/images/emoji/fountain.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/four.png b/app/assets/images/emoji/four.png
deleted file mode 100644
index b0e914aac45..00000000000
--- a/app/assets/images/emoji/four.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/four_leaf_clover.png b/app/assets/images/emoji/four_leaf_clover.png
deleted file mode 100644
index fdedfcc2b4e..00000000000
--- a/app/assets/images/emoji/four_leaf_clover.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fox.png b/app/assets/images/emoji/fox.png
deleted file mode 100644
index 1ab339bf054..00000000000
--- a/app/assets/images/emoji/fox.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/frame_photo.png b/app/assets/images/emoji/frame_photo.png
deleted file mode 100644
index 9fe84607bfd..00000000000
--- a/app/assets/images/emoji/frame_photo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/free.png b/app/assets/images/emoji/free.png
deleted file mode 100644
index b71956eb48a..00000000000
--- a/app/assets/images/emoji/free.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/french_bread.png b/app/assets/images/emoji/french_bread.png
deleted file mode 100644
index 4c2c5639822..00000000000
--- a/app/assets/images/emoji/french_bread.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fried_shrimp.png b/app/assets/images/emoji/fried_shrimp.png
deleted file mode 100644
index 752ba7f1398..00000000000
--- a/app/assets/images/emoji/fried_shrimp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fries.png b/app/assets/images/emoji/fries.png
deleted file mode 100644
index 4e2a4caacef..00000000000
--- a/app/assets/images/emoji/fries.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/frog.png b/app/assets/images/emoji/frog.png
deleted file mode 100644
index 8825d1ad577..00000000000
--- a/app/assets/images/emoji/frog.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/frowning.png b/app/assets/images/emoji/frowning.png
deleted file mode 100644
index 43ab6b0a1c1..00000000000
--- a/app/assets/images/emoji/frowning.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/frowning2.png b/app/assets/images/emoji/frowning2.png
deleted file mode 100644
index 6ae71f233b9..00000000000
--- a/app/assets/images/emoji/frowning2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fuelpump.png b/app/assets/images/emoji/fuelpump.png
deleted file mode 100644
index 05b18794474..00000000000
--- a/app/assets/images/emoji/fuelpump.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/full_moon.png b/app/assets/images/emoji/full_moon.png
deleted file mode 100644
index c9a2d6aa7c9..00000000000
--- a/app/assets/images/emoji/full_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/full_moon_with_face.png b/app/assets/images/emoji/full_moon_with_face.png
deleted file mode 100644
index a5c25bbaf64..00000000000
--- a/app/assets/images/emoji/full_moon_with_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/game_die.png b/app/assets/images/emoji/game_die.png
deleted file mode 100644
index ad3626fe5e5..00000000000
--- a/app/assets/images/emoji/game_die.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gay_pride_flag.png b/app/assets/images/emoji/gay_pride_flag.png
deleted file mode 100644
index 1bec5f2ffd7..00000000000
--- a/app/assets/images/emoji/gay_pride_flag.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gear.png b/app/assets/images/emoji/gear.png
deleted file mode 100644
index 2a1cc2c0ff4..00000000000
--- a/app/assets/images/emoji/gear.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gem.png b/app/assets/images/emoji/gem.png
deleted file mode 100644
index db122d26a19..00000000000
--- a/app/assets/images/emoji/gem.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gemini.png b/app/assets/images/emoji/gemini.png
deleted file mode 100644
index 1a09698cf00..00000000000
--- a/app/assets/images/emoji/gemini.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ghost.png b/app/assets/images/emoji/ghost.png
deleted file mode 100644
index 5650bc0ed18..00000000000
--- a/app/assets/images/emoji/ghost.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gift.png b/app/assets/images/emoji/gift.png
deleted file mode 100644
index 844e2164560..00000000000
--- a/app/assets/images/emoji/gift.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gift_heart.png b/app/assets/images/emoji/gift_heart.png
deleted file mode 100644
index 902ceafe4d1..00000000000
--- a/app/assets/images/emoji/gift_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/girl.png b/app/assets/images/emoji/girl.png
deleted file mode 100644
index dc1d4d08b39..00000000000
--- a/app/assets/images/emoji/girl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone1.png b/app/assets/images/emoji/girl_tone1.png
deleted file mode 100644
index bb667e88651..00000000000
--- a/app/assets/images/emoji/girl_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone2.png b/app/assets/images/emoji/girl_tone2.png
deleted file mode 100644
index a59ed4a3f0d..00000000000
--- a/app/assets/images/emoji/girl_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone3.png b/app/assets/images/emoji/girl_tone3.png
deleted file mode 100644
index 517e7f2a7b0..00000000000
--- a/app/assets/images/emoji/girl_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone4.png b/app/assets/images/emoji/girl_tone4.png
deleted file mode 100644
index 542d96c8487..00000000000
--- a/app/assets/images/emoji/girl_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone5.png b/app/assets/images/emoji/girl_tone5.png
deleted file mode 100644
index 66b7c28c2df..00000000000
--- a/app/assets/images/emoji/girl_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/globe_with_meridians.png b/app/assets/images/emoji/globe_with_meridians.png
deleted file mode 100644
index 82450c1a4ba..00000000000
--- a/app/assets/images/emoji/globe_with_meridians.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/goal.png b/app/assets/images/emoji/goal.png
deleted file mode 100644
index df3a53da0fb..00000000000
--- a/app/assets/images/emoji/goal.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/goat.png b/app/assets/images/emoji/goat.png
deleted file mode 100644
index f9d9e38a128..00000000000
--- a/app/assets/images/emoji/goat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/golf.png b/app/assets/images/emoji/golf.png
deleted file mode 100644
index f65a21d8a46..00000000000
--- a/app/assets/images/emoji/golf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/golfer.png b/app/assets/images/emoji/golfer.png
deleted file mode 100644
index 39c552de86d..00000000000
--- a/app/assets/images/emoji/golfer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gorilla.png b/app/assets/images/emoji/gorilla.png
deleted file mode 100644
index acc51e13622..00000000000
--- a/app/assets/images/emoji/gorilla.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/grapes.png b/app/assets/images/emoji/grapes.png
deleted file mode 100644
index 30d22218896..00000000000
--- a/app/assets/images/emoji/grapes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/green_apple.png b/app/assets/images/emoji/green_apple.png
deleted file mode 100644
index 5fd51bd3915..00000000000
--- a/app/assets/images/emoji/green_apple.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/green_book.png b/app/assets/images/emoji/green_book.png
deleted file mode 100644
index e5e411cf3b5..00000000000
--- a/app/assets/images/emoji/green_book.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/green_heart.png b/app/assets/images/emoji/green_heart.png
deleted file mode 100644
index c52d60a58be..00000000000
--- a/app/assets/images/emoji/green_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/grey_exclamation.png b/app/assets/images/emoji/grey_exclamation.png
deleted file mode 100644
index 9b64da8bf7f..00000000000
--- a/app/assets/images/emoji/grey_exclamation.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/grey_question.png b/app/assets/images/emoji/grey_question.png
deleted file mode 100644
index 6e7824c75f6..00000000000
--- a/app/assets/images/emoji/grey_question.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/grimacing.png b/app/assets/images/emoji/grimacing.png
deleted file mode 100644
index 871b2f071c9..00000000000
--- a/app/assets/images/emoji/grimacing.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/grin.png b/app/assets/images/emoji/grin.png
deleted file mode 100644
index 418d94c811b..00000000000
--- a/app/assets/images/emoji/grin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/grinning.png b/app/assets/images/emoji/grinning.png
deleted file mode 100644
index 3e8e0dab78c..00000000000
--- a/app/assets/images/emoji/grinning.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guardsman.png b/app/assets/images/emoji/guardsman.png
deleted file mode 100644
index 8d7ab3c473c..00000000000
--- a/app/assets/images/emoji/guardsman.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone1.png b/app/assets/images/emoji/guardsman_tone1.png
deleted file mode 100644
index cea9ba27468..00000000000
--- a/app/assets/images/emoji/guardsman_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone2.png b/app/assets/images/emoji/guardsman_tone2.png
deleted file mode 100644
index 037464e4028..00000000000
--- a/app/assets/images/emoji/guardsman_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone3.png b/app/assets/images/emoji/guardsman_tone3.png
deleted file mode 100644
index 0f6726fbe87..00000000000
--- a/app/assets/images/emoji/guardsman_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone4.png b/app/assets/images/emoji/guardsman_tone4.png
deleted file mode 100644
index 85fcf9a3b97..00000000000
--- a/app/assets/images/emoji/guardsman_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone5.png b/app/assets/images/emoji/guardsman_tone5.png
deleted file mode 100644
index e5f9ca7d5a2..00000000000
--- a/app/assets/images/emoji/guardsman_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guitar.png b/app/assets/images/emoji/guitar.png
deleted file mode 100644
index 43d752f1e3d..00000000000
--- a/app/assets/images/emoji/guitar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gun.png b/app/assets/images/emoji/gun.png
deleted file mode 100644
index 89c5c244c7b..00000000000
--- a/app/assets/images/emoji/gun.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/haircut.png b/app/assets/images/emoji/haircut.png
deleted file mode 100644
index 91266b12930..00000000000
--- a/app/assets/images/emoji/haircut.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone1.png b/app/assets/images/emoji/haircut_tone1.png
deleted file mode 100644
index c743b74abeb..00000000000
--- a/app/assets/images/emoji/haircut_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone2.png b/app/assets/images/emoji/haircut_tone2.png
deleted file mode 100644
index f144f8e55ce..00000000000
--- a/app/assets/images/emoji/haircut_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone3.png b/app/assets/images/emoji/haircut_tone3.png
deleted file mode 100644
index d5ad19563ac..00000000000
--- a/app/assets/images/emoji/haircut_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone4.png b/app/assets/images/emoji/haircut_tone4.png
deleted file mode 100644
index 244fd3af008..00000000000
--- a/app/assets/images/emoji/haircut_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone5.png b/app/assets/images/emoji/haircut_tone5.png
deleted file mode 100644
index 20a94a88623..00000000000
--- a/app/assets/images/emoji/haircut_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hamburger.png b/app/assets/images/emoji/hamburger.png
deleted file mode 100644
index 3573b28a1fd..00000000000
--- a/app/assets/images/emoji/hamburger.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hammer.png b/app/assets/images/emoji/hammer.png
deleted file mode 100644
index 00736cce47d..00000000000
--- a/app/assets/images/emoji/hammer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hammer_pick.png b/app/assets/images/emoji/hammer_pick.png
deleted file mode 100644
index 3bee30ec588..00000000000
--- a/app/assets/images/emoji/hammer_pick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hamster.png b/app/assets/images/emoji/hamster.png
deleted file mode 100644
index 9a04388e4e7..00000000000
--- a/app/assets/images/emoji/hamster.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed.png b/app/assets/images/emoji/hand_splayed.png
deleted file mode 100644
index fb5ae8ebb5a..00000000000
--- a/app/assets/images/emoji/hand_splayed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone1.png b/app/assets/images/emoji/hand_splayed_tone1.png
deleted file mode 100644
index a7888e6bd23..00000000000
--- a/app/assets/images/emoji/hand_splayed_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone2.png b/app/assets/images/emoji/hand_splayed_tone2.png
deleted file mode 100644
index cc10fbc272d..00000000000
--- a/app/assets/images/emoji/hand_splayed_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone3.png b/app/assets/images/emoji/hand_splayed_tone3.png
deleted file mode 100644
index 707236ae8a4..00000000000
--- a/app/assets/images/emoji/hand_splayed_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone4.png b/app/assets/images/emoji/hand_splayed_tone4.png
deleted file mode 100644
index 1430df9c61f..00000000000
--- a/app/assets/images/emoji/hand_splayed_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone5.png b/app/assets/images/emoji/hand_splayed_tone5.png
deleted file mode 100644
index 80bec971b6b..00000000000
--- a/app/assets/images/emoji/hand_splayed_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handbag.png b/app/assets/images/emoji/handbag.png
deleted file mode 100644
index cbf75c5d25e..00000000000
--- a/app/assets/images/emoji/handbag.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handball.png b/app/assets/images/emoji/handball.png
deleted file mode 100644
index 1152f1344c7..00000000000
--- a/app/assets/images/emoji/handball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone1.png b/app/assets/images/emoji/handball_tone1.png
deleted file mode 100644
index c26cac2df98..00000000000
--- a/app/assets/images/emoji/handball_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone2.png b/app/assets/images/emoji/handball_tone2.png
deleted file mode 100644
index 7baaf95a9a2..00000000000
--- a/app/assets/images/emoji/handball_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone3.png b/app/assets/images/emoji/handball_tone3.png
deleted file mode 100644
index 0e3a37c3d40..00000000000
--- a/app/assets/images/emoji/handball_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone4.png b/app/assets/images/emoji/handball_tone4.png
deleted file mode 100644
index e1233f38266..00000000000
--- a/app/assets/images/emoji/handball_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone5.png b/app/assets/images/emoji/handball_tone5.png
deleted file mode 100644
index 6b1eb9b64b0..00000000000
--- a/app/assets/images/emoji/handball_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handshake.png b/app/assets/images/emoji/handshake.png
deleted file mode 100644
index c5d35fd8138..00000000000
--- a/app/assets/images/emoji/handshake.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone1.png b/app/assets/images/emoji/handshake_tone1.png
deleted file mode 100644
index 8f8fbb9bdca..00000000000
--- a/app/assets/images/emoji/handshake_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone2.png b/app/assets/images/emoji/handshake_tone2.png
deleted file mode 100644
index 336a77a6d78..00000000000
--- a/app/assets/images/emoji/handshake_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone3.png b/app/assets/images/emoji/handshake_tone3.png
deleted file mode 100644
index 95f62d4fecd..00000000000
--- a/app/assets/images/emoji/handshake_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone4.png b/app/assets/images/emoji/handshake_tone4.png
deleted file mode 100644
index 2b0a6433886..00000000000
--- a/app/assets/images/emoji/handshake_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone5.png b/app/assets/images/emoji/handshake_tone5.png
deleted file mode 100644
index 40189ee68e4..00000000000
--- a/app/assets/images/emoji/handshake_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hash.png b/app/assets/images/emoji/hash.png
deleted file mode 100644
index 6e26f0070b0..00000000000
--- a/app/assets/images/emoji/hash.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hatched_chick.png b/app/assets/images/emoji/hatched_chick.png
deleted file mode 100644
index 31dfb511e0e..00000000000
--- a/app/assets/images/emoji/hatched_chick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hatching_chick.png b/app/assets/images/emoji/hatching_chick.png
deleted file mode 100644
index c5b0e8f3bcc..00000000000
--- a/app/assets/images/emoji/hatching_chick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/head_bandage.png b/app/assets/images/emoji/head_bandage.png
deleted file mode 100644
index 0be723085e0..00000000000
--- a/app/assets/images/emoji/head_bandage.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/headphones.png b/app/assets/images/emoji/headphones.png
deleted file mode 100644
index e9fd34041d8..00000000000
--- a/app/assets/images/emoji/headphones.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hear_no_evil.png b/app/assets/images/emoji/hear_no_evil.png
deleted file mode 100644
index 74b6be0c6c5..00000000000
--- a/app/assets/images/emoji/hear_no_evil.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heart.png b/app/assets/images/emoji/heart.png
deleted file mode 100644
index 638cb72dc4e..00000000000
--- a/app/assets/images/emoji/heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heart_decoration.png b/app/assets/images/emoji/heart_decoration.png
deleted file mode 100644
index 5443f60bc63..00000000000
--- a/app/assets/images/emoji/heart_decoration.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heart_exclamation.png b/app/assets/images/emoji/heart_exclamation.png
deleted file mode 100644
index 91b520be40b..00000000000
--- a/app/assets/images/emoji/heart_exclamation.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heart_eyes.png b/app/assets/images/emoji/heart_eyes.png
deleted file mode 100644
index 73fbee29d4e..00000000000
--- a/app/assets/images/emoji/heart_eyes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heart_eyes_cat.png b/app/assets/images/emoji/heart_eyes_cat.png
deleted file mode 100644
index bc5a833f9a1..00000000000
--- a/app/assets/images/emoji/heart_eyes_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heartbeat.png b/app/assets/images/emoji/heartbeat.png
deleted file mode 100644
index 0bcf2d1d567..00000000000
--- a/app/assets/images/emoji/heartbeat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heartpulse.png b/app/assets/images/emoji/heartpulse.png
deleted file mode 100644
index d6e694e972f..00000000000
--- a/app/assets/images/emoji/heartpulse.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hearts.png b/app/assets/images/emoji/hearts.png
deleted file mode 100644
index 393c3ed5267..00000000000
--- a/app/assets/images/emoji/hearts.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heavy_check_mark.png b/app/assets/images/emoji/heavy_check_mark.png
deleted file mode 100644
index 03bd695377e..00000000000
--- a/app/assets/images/emoji/heavy_check_mark.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heavy_division_sign.png b/app/assets/images/emoji/heavy_division_sign.png
deleted file mode 100644
index df32ab21bea..00000000000
--- a/app/assets/images/emoji/heavy_division_sign.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heavy_dollar_sign.png b/app/assets/images/emoji/heavy_dollar_sign.png
deleted file mode 100644
index ef2c2e20590..00000000000
--- a/app/assets/images/emoji/heavy_dollar_sign.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heavy_minus_sign.png b/app/assets/images/emoji/heavy_minus_sign.png
deleted file mode 100644
index 054211caf12..00000000000
--- a/app/assets/images/emoji/heavy_minus_sign.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heavy_multiplication_x.png b/app/assets/images/emoji/heavy_multiplication_x.png
deleted file mode 100644
index e47cc1b685d..00000000000
--- a/app/assets/images/emoji/heavy_multiplication_x.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heavy_plus_sign.png b/app/assets/images/emoji/heavy_plus_sign.png
deleted file mode 100644
index 40799798aaf..00000000000
--- a/app/assets/images/emoji/heavy_plus_sign.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/helicopter.png b/app/assets/images/emoji/helicopter.png
deleted file mode 100644
index 7ec5f39a51a..00000000000
--- a/app/assets/images/emoji/helicopter.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/helmet_with_cross.png b/app/assets/images/emoji/helmet_with_cross.png
deleted file mode 100644
index 7140a676038..00000000000
--- a/app/assets/images/emoji/helmet_with_cross.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/herb.png b/app/assets/images/emoji/herb.png
deleted file mode 100644
index d984d1562bb..00000000000
--- a/app/assets/images/emoji/herb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hibiscus.png b/app/assets/images/emoji/hibiscus.png
deleted file mode 100644
index 39dd3524233..00000000000
--- a/app/assets/images/emoji/hibiscus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/high_brightness.png b/app/assets/images/emoji/high_brightness.png
deleted file mode 100644
index c41f2d5fd50..00000000000
--- a/app/assets/images/emoji/high_brightness.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/high_heel.png b/app/assets/images/emoji/high_heel.png
deleted file mode 100644
index b331cbccc9d..00000000000
--- a/app/assets/images/emoji/high_heel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hockey.png b/app/assets/images/emoji/hockey.png
deleted file mode 100644
index be94e9cbf73..00000000000
--- a/app/assets/images/emoji/hockey.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hole.png b/app/assets/images/emoji/hole.png
deleted file mode 100644
index 517d2ae0deb..00000000000
--- a/app/assets/images/emoji/hole.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/homes.png b/app/assets/images/emoji/homes.png
deleted file mode 100644
index 6ab4a2a2651..00000000000
--- a/app/assets/images/emoji/homes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/honey_pot.png b/app/assets/images/emoji/honey_pot.png
deleted file mode 100644
index 9d8f592955e..00000000000
--- a/app/assets/images/emoji/honey_pot.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse.png b/app/assets/images/emoji/horse.png
deleted file mode 100644
index 7cb1172f4e4..00000000000
--- a/app/assets/images/emoji/horse.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing.png b/app/assets/images/emoji/horse_racing.png
deleted file mode 100644
index addf9edac56..00000000000
--- a/app/assets/images/emoji/horse_racing.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone1.png b/app/assets/images/emoji/horse_racing_tone1.png
deleted file mode 100644
index e9bf4092e98..00000000000
--- a/app/assets/images/emoji/horse_racing_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone2.png b/app/assets/images/emoji/horse_racing_tone2.png
deleted file mode 100644
index 031bbc3d867..00000000000
--- a/app/assets/images/emoji/horse_racing_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone3.png b/app/assets/images/emoji/horse_racing_tone3.png
deleted file mode 100644
index b40ef891f9b..00000000000
--- a/app/assets/images/emoji/horse_racing_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone4.png b/app/assets/images/emoji/horse_racing_tone4.png
deleted file mode 100644
index e286cb85065..00000000000
--- a/app/assets/images/emoji/horse_racing_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone5.png b/app/assets/images/emoji/horse_racing_tone5.png
deleted file mode 100644
index 453c51c6007..00000000000
--- a/app/assets/images/emoji/horse_racing_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hospital.png b/app/assets/images/emoji/hospital.png
deleted file mode 100644
index 1cbce4ae767..00000000000
--- a/app/assets/images/emoji/hospital.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hot_pepper.png b/app/assets/images/emoji/hot_pepper.png
deleted file mode 100644
index 266675bd577..00000000000
--- a/app/assets/images/emoji/hot_pepper.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hotdog.png b/app/assets/images/emoji/hotdog.png
deleted file mode 100644
index 3c3354d94cb..00000000000
--- a/app/assets/images/emoji/hotdog.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hotel.png b/app/assets/images/emoji/hotel.png
deleted file mode 100644
index ea8f4c4979a..00000000000
--- a/app/assets/images/emoji/hotel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hotsprings.png b/app/assets/images/emoji/hotsprings.png
deleted file mode 100644
index 3d9df2d9475..00000000000
--- a/app/assets/images/emoji/hotsprings.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hourglass.png b/app/assets/images/emoji/hourglass.png
deleted file mode 100644
index a5db2d1d3f4..00000000000
--- a/app/assets/images/emoji/hourglass.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hourglass_flowing_sand.png b/app/assets/images/emoji/hourglass_flowing_sand.png
deleted file mode 100644
index b93b15ed6d8..00000000000
--- a/app/assets/images/emoji/hourglass_flowing_sand.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/house.png b/app/assets/images/emoji/house.png
deleted file mode 100644
index 01c98a0ba92..00000000000
--- a/app/assets/images/emoji/house.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/house_abandoned.png b/app/assets/images/emoji/house_abandoned.png
deleted file mode 100644
index c55e81de990..00000000000
--- a/app/assets/images/emoji/house_abandoned.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/house_with_garden.png b/app/assets/images/emoji/house_with_garden.png
deleted file mode 100644
index 0aae41598ef..00000000000
--- a/app/assets/images/emoji/house_with_garden.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hugging.png b/app/assets/images/emoji/hugging.png
deleted file mode 100644
index 5bba6dc6d51..00000000000
--- a/app/assets/images/emoji/hugging.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hushed.png b/app/assets/images/emoji/hushed.png
deleted file mode 100644
index cad0e23132e..00000000000
--- a/app/assets/images/emoji/hushed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ice_cream.png b/app/assets/images/emoji/ice_cream.png
deleted file mode 100644
index 94267b9c434..00000000000
--- a/app/assets/images/emoji/ice_cream.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ice_skate.png b/app/assets/images/emoji/ice_skate.png
deleted file mode 100644
index 8c449b0c039..00000000000
--- a/app/assets/images/emoji/ice_skate.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/icecream.png b/app/assets/images/emoji/icecream.png
deleted file mode 100644
index 8f6546e31a5..00000000000
--- a/app/assets/images/emoji/icecream.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/id.png b/app/assets/images/emoji/id.png
deleted file mode 100644
index 5bf69bf7ba8..00000000000
--- a/app/assets/images/emoji/id.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ideograph_advantage.png b/app/assets/images/emoji/ideograph_advantage.png
deleted file mode 100644
index 0c0d589caf0..00000000000
--- a/app/assets/images/emoji/ideograph_advantage.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/imp.png b/app/assets/images/emoji/imp.png
deleted file mode 100644
index 9f9a9605539..00000000000
--- a/app/assets/images/emoji/imp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/inbox_tray.png b/app/assets/images/emoji/inbox_tray.png
deleted file mode 100644
index 41a6be2b0ee..00000000000
--- a/app/assets/images/emoji/inbox_tray.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/incoming_envelope.png b/app/assets/images/emoji/incoming_envelope.png
deleted file mode 100644
index fd22e88182e..00000000000
--- a/app/assets/images/emoji/incoming_envelope.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person.png b/app/assets/images/emoji/information_desk_person.png
deleted file mode 100644
index 55fc6294d25..00000000000
--- a/app/assets/images/emoji/information_desk_person.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone1.png b/app/assets/images/emoji/information_desk_person_tone1.png
deleted file mode 100644
index 3d9e2247940..00000000000
--- a/app/assets/images/emoji/information_desk_person_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone2.png b/app/assets/images/emoji/information_desk_person_tone2.png
deleted file mode 100644
index 879e8b7966d..00000000000
--- a/app/assets/images/emoji/information_desk_person_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone3.png b/app/assets/images/emoji/information_desk_person_tone3.png
deleted file mode 100644
index 307514eab67..00000000000
--- a/app/assets/images/emoji/information_desk_person_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone4.png b/app/assets/images/emoji/information_desk_person_tone4.png
deleted file mode 100644
index 297395dcb3f..00000000000
--- a/app/assets/images/emoji/information_desk_person_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone5.png b/app/assets/images/emoji/information_desk_person_tone5.png
deleted file mode 100644
index 26f8f22b28b..00000000000
--- a/app/assets/images/emoji/information_desk_person_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_source.png b/app/assets/images/emoji/information_source.png
deleted file mode 100644
index 871f2db9314..00000000000
--- a/app/assets/images/emoji/information_source.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/innocent.png b/app/assets/images/emoji/innocent.png
deleted file mode 100644
index 57f5151124f..00000000000
--- a/app/assets/images/emoji/innocent.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/interrobang.png b/app/assets/images/emoji/interrobang.png
deleted file mode 100644
index 509813e9bb2..00000000000
--- a/app/assets/images/emoji/interrobang.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/iphone.png b/app/assets/images/emoji/iphone.png
deleted file mode 100644
index fd377acf872..00000000000
--- a/app/assets/images/emoji/iphone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/island.png b/app/assets/images/emoji/island.png
deleted file mode 100644
index 7fd834389b7..00000000000
--- a/app/assets/images/emoji/island.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/izakaya_lantern.png b/app/assets/images/emoji/izakaya_lantern.png
deleted file mode 100644
index dfd933f6f36..00000000000
--- a/app/assets/images/emoji/izakaya_lantern.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/jack_o_lantern.png b/app/assets/images/emoji/jack_o_lantern.png
deleted file mode 100644
index 44c3fc0aec9..00000000000
--- a/app/assets/images/emoji/jack_o_lantern.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/japan.png b/app/assets/images/emoji/japan.png
deleted file mode 100644
index d86d0a59e12..00000000000
--- a/app/assets/images/emoji/japan.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/japanese_castle.png b/app/assets/images/emoji/japanese_castle.png
deleted file mode 100644
index 64b4e33a1ae..00000000000
--- a/app/assets/images/emoji/japanese_castle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/japanese_goblin.png b/app/assets/images/emoji/japanese_goblin.png
deleted file mode 100644
index 515c6a2250e..00000000000
--- a/app/assets/images/emoji/japanese_goblin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/japanese_ogre.png b/app/assets/images/emoji/japanese_ogre.png
deleted file mode 100644
index fe8670fdaf1..00000000000
--- a/app/assets/images/emoji/japanese_ogre.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/jeans.png b/app/assets/images/emoji/jeans.png
deleted file mode 100644
index 2a6869d674c..00000000000
--- a/app/assets/images/emoji/jeans.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/joy.png b/app/assets/images/emoji/joy.png
deleted file mode 100644
index 0ba3b1859d8..00000000000
--- a/app/assets/images/emoji/joy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/joy_cat.png b/app/assets/images/emoji/joy_cat.png
deleted file mode 100644
index aac353179aa..00000000000
--- a/app/assets/images/emoji/joy_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/joystick.png b/app/assets/images/emoji/joystick.png
deleted file mode 100644
index 1ee1905434e..00000000000
--- a/app/assets/images/emoji/joystick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/juggling.png b/app/assets/images/emoji/juggling.png
deleted file mode 100644
index a37f6224a42..00000000000
--- a/app/assets/images/emoji/juggling.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone1.png b/app/assets/images/emoji/juggling_tone1.png
deleted file mode 100644
index c18eda40031..00000000000
--- a/app/assets/images/emoji/juggling_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone2.png b/app/assets/images/emoji/juggling_tone2.png
deleted file mode 100644
index de3b7a555b6..00000000000
--- a/app/assets/images/emoji/juggling_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone3.png b/app/assets/images/emoji/juggling_tone3.png
deleted file mode 100644
index 74ab6d85458..00000000000
--- a/app/assets/images/emoji/juggling_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone4.png b/app/assets/images/emoji/juggling_tone4.png
deleted file mode 100644
index 1c57823203f..00000000000
--- a/app/assets/images/emoji/juggling_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone5.png b/app/assets/images/emoji/juggling_tone5.png
deleted file mode 100644
index c343d6ee98a..00000000000
--- a/app/assets/images/emoji/juggling_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kaaba.png b/app/assets/images/emoji/kaaba.png
deleted file mode 100644
index 1778c1138e4..00000000000
--- a/app/assets/images/emoji/kaaba.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/key.png b/app/assets/images/emoji/key.png
deleted file mode 100644
index 319cd1b884c..00000000000
--- a/app/assets/images/emoji/key.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/key2.png b/app/assets/images/emoji/key2.png
deleted file mode 100644
index e11d706c6c8..00000000000
--- a/app/assets/images/emoji/key2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/keyboard.png b/app/assets/images/emoji/keyboard.png
deleted file mode 100644
index 75027cb9af7..00000000000
--- a/app/assets/images/emoji/keyboard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kimono.png b/app/assets/images/emoji/kimono.png
deleted file mode 100644
index abe851115d1..00000000000
--- a/app/assets/images/emoji/kimono.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kiss.png b/app/assets/images/emoji/kiss.png
deleted file mode 100644
index 85e6dcfc4e8..00000000000
--- a/app/assets/images/emoji/kiss.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kiss_mm.png b/app/assets/images/emoji/kiss_mm.png
deleted file mode 100644
index a9a0edae17c..00000000000
--- a/app/assets/images/emoji/kiss_mm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kiss_ww.png b/app/assets/images/emoji/kiss_ww.png
deleted file mode 100644
index fdac73cbb1d..00000000000
--- a/app/assets/images/emoji/kiss_ww.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kissing.png b/app/assets/images/emoji/kissing.png
deleted file mode 100644
index 39d325fd8e3..00000000000
--- a/app/assets/images/emoji/kissing.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kissing_cat.png b/app/assets/images/emoji/kissing_cat.png
deleted file mode 100644
index 6e0bcc77540..00000000000
--- a/app/assets/images/emoji/kissing_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kissing_closed_eyes.png b/app/assets/images/emoji/kissing_closed_eyes.png
deleted file mode 100644
index b684d7d4d6c..00000000000
--- a/app/assets/images/emoji/kissing_closed_eyes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kissing_heart.png b/app/assets/images/emoji/kissing_heart.png
deleted file mode 100644
index 0ff808fd614..00000000000
--- a/app/assets/images/emoji/kissing_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kissing_smiling_eyes.png b/app/assets/images/emoji/kissing_smiling_eyes.png
deleted file mode 100644
index e181f17099d..00000000000
--- a/app/assets/images/emoji/kissing_smiling_eyes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kiwi.png b/app/assets/images/emoji/kiwi.png
deleted file mode 100644
index dfbd8258074..00000000000
--- a/app/assets/images/emoji/kiwi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/knife.png b/app/assets/images/emoji/knife.png
deleted file mode 100644
index 1acb9f3077b..00000000000
--- a/app/assets/images/emoji/knife.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/koala.png b/app/assets/images/emoji/koala.png
deleted file mode 100644
index a0aa437a98c..00000000000
--- a/app/assets/images/emoji/koala.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/koko.png b/app/assets/images/emoji/koko.png
deleted file mode 100644
index 6450eb44d90..00000000000
--- a/app/assets/images/emoji/koko.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/label.png b/app/assets/images/emoji/label.png
deleted file mode 100644
index d41c9b4f1e1..00000000000
--- a/app/assets/images/emoji/label.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/large_blue_circle.png b/app/assets/images/emoji/large_blue_circle.png
deleted file mode 100644
index 84078ef3127..00000000000
--- a/app/assets/images/emoji/large_blue_circle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/large_blue_diamond.png b/app/assets/images/emoji/large_blue_diamond.png
deleted file mode 100644
index 416a58bd5a8..00000000000
--- a/app/assets/images/emoji/large_blue_diamond.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/large_orange_diamond.png b/app/assets/images/emoji/large_orange_diamond.png
deleted file mode 100644
index 73ff0ac36c8..00000000000
--- a/app/assets/images/emoji/large_orange_diamond.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/last_quarter_moon.png b/app/assets/images/emoji/last_quarter_moon.png
deleted file mode 100644
index 0842a0dd408..00000000000
--- a/app/assets/images/emoji/last_quarter_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/last_quarter_moon_with_face.png b/app/assets/images/emoji/last_quarter_moon_with_face.png
deleted file mode 100644
index 94099343c5d..00000000000
--- a/app/assets/images/emoji/last_quarter_moon_with_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/laughing.png b/app/assets/images/emoji/laughing.png
deleted file mode 100644
index d94e9505ba1..00000000000
--- a/app/assets/images/emoji/laughing.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/leaves.png b/app/assets/images/emoji/leaves.png
deleted file mode 100644
index 1e43e1af820..00000000000
--- a/app/assets/images/emoji/leaves.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ledger.png b/app/assets/images/emoji/ledger.png
deleted file mode 100644
index 13e7561a4bd..00000000000
--- a/app/assets/images/emoji/ledger.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist.png b/app/assets/images/emoji/left_facing_fist.png
deleted file mode 100644
index a9d9fd8d59c..00000000000
--- a/app/assets/images/emoji/left_facing_fist.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone1.png b/app/assets/images/emoji/left_facing_fist_tone1.png
deleted file mode 100644
index 1262a6b4b69..00000000000
--- a/app/assets/images/emoji/left_facing_fist_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone2.png b/app/assets/images/emoji/left_facing_fist_tone2.png
deleted file mode 100644
index 40bf70b82b2..00000000000
--- a/app/assets/images/emoji/left_facing_fist_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone3.png b/app/assets/images/emoji/left_facing_fist_tone3.png
deleted file mode 100644
index 93f58145111..00000000000
--- a/app/assets/images/emoji/left_facing_fist_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone4.png b/app/assets/images/emoji/left_facing_fist_tone4.png
deleted file mode 100644
index d82b5ec91f0..00000000000
--- a/app/assets/images/emoji/left_facing_fist_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone5.png b/app/assets/images/emoji/left_facing_fist_tone5.png
deleted file mode 100644
index 09ae4cd492b..00000000000
--- a/app/assets/images/emoji/left_facing_fist_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_luggage.png b/app/assets/images/emoji/left_luggage.png
deleted file mode 100644
index 887b23f3f25..00000000000
--- a/app/assets/images/emoji/left_luggage.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_right_arrow.png b/app/assets/images/emoji/left_right_arrow.png
deleted file mode 100644
index 7937f24f2ac..00000000000
--- a/app/assets/images/emoji/left_right_arrow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/leftwards_arrow_with_hook.png b/app/assets/images/emoji/leftwards_arrow_with_hook.png
deleted file mode 100644
index ba45c2ad9e9..00000000000
--- a/app/assets/images/emoji/leftwards_arrow_with_hook.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lemon.png b/app/assets/images/emoji/lemon.png
deleted file mode 100644
index 9a7d95ca220..00000000000
--- a/app/assets/images/emoji/lemon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/leo.png b/app/assets/images/emoji/leo.png
deleted file mode 100644
index 30158d34de9..00000000000
--- a/app/assets/images/emoji/leo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/leopard.png b/app/assets/images/emoji/leopard.png
deleted file mode 100644
index 8aac3d49448..00000000000
--- a/app/assets/images/emoji/leopard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/level_slider.png b/app/assets/images/emoji/level_slider.png
deleted file mode 100644
index 720a3b34119..00000000000
--- a/app/assets/images/emoji/level_slider.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/levitate.png b/app/assets/images/emoji/levitate.png
deleted file mode 100644
index 3dc315a3d91..00000000000
--- a/app/assets/images/emoji/levitate.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/libra.png b/app/assets/images/emoji/libra.png
deleted file mode 100644
index 8fd133a357c..00000000000
--- a/app/assets/images/emoji/libra.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lifter.png b/app/assets/images/emoji/lifter.png
deleted file mode 100644
index afdeaa476af..00000000000
--- a/app/assets/images/emoji/lifter.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone1.png b/app/assets/images/emoji/lifter_tone1.png
deleted file mode 100644
index febaad123ec..00000000000
--- a/app/assets/images/emoji/lifter_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone2.png b/app/assets/images/emoji/lifter_tone2.png
deleted file mode 100644
index 27ae794a18e..00000000000
--- a/app/assets/images/emoji/lifter_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone3.png b/app/assets/images/emoji/lifter_tone3.png
deleted file mode 100644
index 45c4c22c709..00000000000
--- a/app/assets/images/emoji/lifter_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone4.png b/app/assets/images/emoji/lifter_tone4.png
deleted file mode 100644
index 67dd21d2464..00000000000
--- a/app/assets/images/emoji/lifter_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone5.png b/app/assets/images/emoji/lifter_tone5.png
deleted file mode 100644
index fa0152038b6..00000000000
--- a/app/assets/images/emoji/lifter_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/light_rail.png b/app/assets/images/emoji/light_rail.png
deleted file mode 100644
index a64829f5078..00000000000
--- a/app/assets/images/emoji/light_rail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/link.png b/app/assets/images/emoji/link.png
deleted file mode 100644
index ae20f0f8eec..00000000000
--- a/app/assets/images/emoji/link.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lion_face.png b/app/assets/images/emoji/lion_face.png
deleted file mode 100644
index 5062ab47ecf..00000000000
--- a/app/assets/images/emoji/lion_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lips.png b/app/assets/images/emoji/lips.png
deleted file mode 100644
index 35f3cc2006f..00000000000
--- a/app/assets/images/emoji/lips.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lipstick.png b/app/assets/images/emoji/lipstick.png
deleted file mode 100644
index 61a0c084c99..00000000000
--- a/app/assets/images/emoji/lipstick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lizard.png b/app/assets/images/emoji/lizard.png
deleted file mode 100644
index 8363876050e..00000000000
--- a/app/assets/images/emoji/lizard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lock.png b/app/assets/images/emoji/lock.png
deleted file mode 100644
index 5a739c46644..00000000000
--- a/app/assets/images/emoji/lock.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lock_with_ink_pen.png b/app/assets/images/emoji/lock_with_ink_pen.png
deleted file mode 100644
index 19a07d162fb..00000000000
--- a/app/assets/images/emoji/lock_with_ink_pen.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lollipop.png b/app/assets/images/emoji/lollipop.png
deleted file mode 100644
index ad76d7bf916..00000000000
--- a/app/assets/images/emoji/lollipop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/loop.png b/app/assets/images/emoji/loop.png
deleted file mode 100644
index 0b82c8fe315..00000000000
--- a/app/assets/images/emoji/loop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/loud_sound.png b/app/assets/images/emoji/loud_sound.png
deleted file mode 100644
index 8370033a539..00000000000
--- a/app/assets/images/emoji/loud_sound.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/loudspeaker.png b/app/assets/images/emoji/loudspeaker.png
deleted file mode 100644
index 5fd76a95b82..00000000000
--- a/app/assets/images/emoji/loudspeaker.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/love_hotel.png b/app/assets/images/emoji/love_hotel.png
deleted file mode 100644
index 5e136be6f8b..00000000000
--- a/app/assets/images/emoji/love_hotel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/love_letter.png b/app/assets/images/emoji/love_letter.png
deleted file mode 100644
index 3c3c767e784..00000000000
--- a/app/assets/images/emoji/love_letter.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/low_brightness.png b/app/assets/images/emoji/low_brightness.png
deleted file mode 100644
index 543011d3961..00000000000
--- a/app/assets/images/emoji/low_brightness.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lying_face.png b/app/assets/images/emoji/lying_face.png
deleted file mode 100644
index 02827e2628b..00000000000
--- a/app/assets/images/emoji/lying_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/m.png b/app/assets/images/emoji/m.png
deleted file mode 100644
index 8a3506fc1d7..00000000000
--- a/app/assets/images/emoji/m.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mag.png b/app/assets/images/emoji/mag.png
deleted file mode 100644
index 55487156ac6..00000000000
--- a/app/assets/images/emoji/mag.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mag_right.png b/app/assets/images/emoji/mag_right.png
deleted file mode 100644
index 0f4b1bca876..00000000000
--- a/app/assets/images/emoji/mag_right.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mahjong.png b/app/assets/images/emoji/mahjong.png
deleted file mode 100644
index 66fd32025b2..00000000000
--- a/app/assets/images/emoji/mahjong.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mailbox.png b/app/assets/images/emoji/mailbox.png
deleted file mode 100644
index ef5174e40dd..00000000000
--- a/app/assets/images/emoji/mailbox.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mailbox_closed.png b/app/assets/images/emoji/mailbox_closed.png
deleted file mode 100644
index ddc705db0d8..00000000000
--- a/app/assets/images/emoji/mailbox_closed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mailbox_with_mail.png b/app/assets/images/emoji/mailbox_with_mail.png
deleted file mode 100644
index 5460616a5b1..00000000000
--- a/app/assets/images/emoji/mailbox_with_mail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mailbox_with_no_mail.png b/app/assets/images/emoji/mailbox_with_no_mail.png
deleted file mode 100644
index f9aeee6b15a..00000000000
--- a/app/assets/images/emoji/mailbox_with_no_mail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man.png b/app/assets/images/emoji/man.png
deleted file mode 100644
index 857a02e5146..00000000000
--- a/app/assets/images/emoji/man.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing.png b/app/assets/images/emoji/man_dancing.png
deleted file mode 100644
index ccff3bede5a..00000000000
--- a/app/assets/images/emoji/man_dancing.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone1.png b/app/assets/images/emoji/man_dancing_tone1.png
deleted file mode 100644
index e0b9f82d905..00000000000
--- a/app/assets/images/emoji/man_dancing_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone2.png b/app/assets/images/emoji/man_dancing_tone2.png
deleted file mode 100644
index a5beed56e2e..00000000000
--- a/app/assets/images/emoji/man_dancing_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone3.png b/app/assets/images/emoji/man_dancing_tone3.png
deleted file mode 100644
index 2fa20180a6e..00000000000
--- a/app/assets/images/emoji/man_dancing_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone4.png b/app/assets/images/emoji/man_dancing_tone4.png
deleted file mode 100644
index bd3528c83ba..00000000000
--- a/app/assets/images/emoji/man_dancing_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone5.png b/app/assets/images/emoji/man_dancing_tone5.png
deleted file mode 100644
index 41fd4f880c9..00000000000
--- a/app/assets/images/emoji/man_dancing_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo.png b/app/assets/images/emoji/man_in_tuxedo.png
deleted file mode 100644
index 5f7e9303f89..00000000000
--- a/app/assets/images/emoji/man_in_tuxedo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone1.png b/app/assets/images/emoji/man_in_tuxedo_tone1.png
deleted file mode 100644
index 7b6b3acd99b..00000000000
--- a/app/assets/images/emoji/man_in_tuxedo_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone2.png b/app/assets/images/emoji/man_in_tuxedo_tone2.png
deleted file mode 100644
index 7975191b360..00000000000
--- a/app/assets/images/emoji/man_in_tuxedo_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone3.png b/app/assets/images/emoji/man_in_tuxedo_tone3.png
deleted file mode 100644
index a2816f600ae..00000000000
--- a/app/assets/images/emoji/man_in_tuxedo_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone4.png b/app/assets/images/emoji/man_in_tuxedo_tone4.png
deleted file mode 100644
index ea8291760f9..00000000000
--- a/app/assets/images/emoji/man_in_tuxedo_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone5.png b/app/assets/images/emoji/man_in_tuxedo_tone5.png
deleted file mode 100644
index c743e05fc5e..00000000000
--- a/app/assets/images/emoji/man_in_tuxedo_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_tone1.png b/app/assets/images/emoji/man_tone1.png
deleted file mode 100644
index bb86e963a80..00000000000
--- a/app/assets/images/emoji/man_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_tone2.png b/app/assets/images/emoji/man_tone2.png
deleted file mode 100644
index fdeeaff46f5..00000000000
--- a/app/assets/images/emoji/man_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_tone3.png b/app/assets/images/emoji/man_tone3.png
deleted file mode 100644
index 7ae0b5df9cf..00000000000
--- a/app/assets/images/emoji/man_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_tone4.png b/app/assets/images/emoji/man_tone4.png
deleted file mode 100644
index db14cde99b8..00000000000
--- a/app/assets/images/emoji/man_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_tone5.png b/app/assets/images/emoji/man_tone5.png
deleted file mode 100644
index 7c67a70529c..00000000000
--- a/app/assets/images/emoji/man_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao.png b/app/assets/images/emoji/man_with_gua_pi_mao.png
deleted file mode 100644
index 7841e13608d..00000000000
--- a/app/assets/images/emoji/man_with_gua_pi_mao.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png
deleted file mode 100644
index 5b7b3def19c..00000000000
--- a/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png
deleted file mode 100644
index c8b9cf87f4b..00000000000
--- a/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png
deleted file mode 100644
index effdd0c4c84..00000000000
--- a/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png
deleted file mode 100644
index f885ff46fa1..00000000000
--- a/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png
deleted file mode 100644
index a6d55ca1380..00000000000
--- a/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban.png b/app/assets/images/emoji/man_with_turban.png
deleted file mode 100644
index 51cf047f966..00000000000
--- a/app/assets/images/emoji/man_with_turban.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone1.png b/app/assets/images/emoji/man_with_turban_tone1.png
deleted file mode 100644
index 1e12ee4b231..00000000000
--- a/app/assets/images/emoji/man_with_turban_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone2.png b/app/assets/images/emoji/man_with_turban_tone2.png
deleted file mode 100644
index 37de4cceb23..00000000000
--- a/app/assets/images/emoji/man_with_turban_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone3.png b/app/assets/images/emoji/man_with_turban_tone3.png
deleted file mode 100644
index f607afd3450..00000000000
--- a/app/assets/images/emoji/man_with_turban_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone4.png b/app/assets/images/emoji/man_with_turban_tone4.png
deleted file mode 100644
index c05695888af..00000000000
--- a/app/assets/images/emoji/man_with_turban_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone5.png b/app/assets/images/emoji/man_with_turban_tone5.png
deleted file mode 100644
index 4b4ff64720b..00000000000
--- a/app/assets/images/emoji/man_with_turban_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mans_shoe.png b/app/assets/images/emoji/mans_shoe.png
deleted file mode 100644
index 4bf7541032c..00000000000
--- a/app/assets/images/emoji/mans_shoe.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/map.png b/app/assets/images/emoji/map.png
deleted file mode 100644
index 15efe32c798..00000000000
--- a/app/assets/images/emoji/map.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/maple_leaf.png b/app/assets/images/emoji/maple_leaf.png
deleted file mode 100644
index c49acea67f7..00000000000
--- a/app/assets/images/emoji/maple_leaf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/martial_arts_uniform.png b/app/assets/images/emoji/martial_arts_uniform.png
deleted file mode 100644
index 8d6114761f6..00000000000
--- a/app/assets/images/emoji/martial_arts_uniform.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mask.png b/app/assets/images/emoji/mask.png
deleted file mode 100644
index 1e800acd1c0..00000000000
--- a/app/assets/images/emoji/mask.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/massage.png b/app/assets/images/emoji/massage.png
deleted file mode 100644
index b91d845e374..00000000000
--- a/app/assets/images/emoji/massage.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone1.png b/app/assets/images/emoji/massage_tone1.png
deleted file mode 100644
index e0f415d3186..00000000000
--- a/app/assets/images/emoji/massage_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone2.png b/app/assets/images/emoji/massage_tone2.png
deleted file mode 100644
index 0bb244a270b..00000000000
--- a/app/assets/images/emoji/massage_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone3.png b/app/assets/images/emoji/massage_tone3.png
deleted file mode 100644
index a117ee81a22..00000000000
--- a/app/assets/images/emoji/massage_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone4.png b/app/assets/images/emoji/massage_tone4.png
deleted file mode 100644
index 6f42ab017f4..00000000000
--- a/app/assets/images/emoji/massage_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone5.png b/app/assets/images/emoji/massage_tone5.png
deleted file mode 100644
index 6a388c0d0b5..00000000000
--- a/app/assets/images/emoji/massage_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/meat_on_bone.png b/app/assets/images/emoji/meat_on_bone.png
deleted file mode 100644
index b20a59d1690..00000000000
--- a/app/assets/images/emoji/meat_on_bone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/medal.png b/app/assets/images/emoji/medal.png
deleted file mode 100644
index b85896b14da..00000000000
--- a/app/assets/images/emoji/medal.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mega.png b/app/assets/images/emoji/mega.png
deleted file mode 100644
index 4e6735188e3..00000000000
--- a/app/assets/images/emoji/mega.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/melon.png b/app/assets/images/emoji/melon.png
deleted file mode 100644
index c01232d419d..00000000000
--- a/app/assets/images/emoji/melon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/menorah.png b/app/assets/images/emoji/menorah.png
deleted file mode 100644
index b4297362869..00000000000
--- a/app/assets/images/emoji/menorah.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mens.png b/app/assets/images/emoji/mens.png
deleted file mode 100644
index f5a1e1ba0cd..00000000000
--- a/app/assets/images/emoji/mens.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metal.png b/app/assets/images/emoji/metal.png
deleted file mode 100644
index 4aa6e7e0a44..00000000000
--- a/app/assets/images/emoji/metal.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone1.png b/app/assets/images/emoji/metal_tone1.png
deleted file mode 100644
index c080d2addbd..00000000000
--- a/app/assets/images/emoji/metal_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone2.png b/app/assets/images/emoji/metal_tone2.png
deleted file mode 100644
index 12313529bcf..00000000000
--- a/app/assets/images/emoji/metal_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone3.png b/app/assets/images/emoji/metal_tone3.png
deleted file mode 100644
index ca9be6ae67b..00000000000
--- a/app/assets/images/emoji/metal_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone4.png b/app/assets/images/emoji/metal_tone4.png
deleted file mode 100644
index abe28cbf890..00000000000
--- a/app/assets/images/emoji/metal_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone5.png b/app/assets/images/emoji/metal_tone5.png
deleted file mode 100644
index 0c6b5dd34ed..00000000000
--- a/app/assets/images/emoji/metal_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metro.png b/app/assets/images/emoji/metro.png
deleted file mode 100644
index 1de8f0551f3..00000000000
--- a/app/assets/images/emoji/metro.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/microphone.png b/app/assets/images/emoji/microphone.png
deleted file mode 100644
index d4e6b0def25..00000000000
--- a/app/assets/images/emoji/microphone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/microphone2.png b/app/assets/images/emoji/microphone2.png
deleted file mode 100644
index cd9167654ff..00000000000
--- a/app/assets/images/emoji/microphone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/microscope.png b/app/assets/images/emoji/microscope.png
deleted file mode 100644
index 90f5acf6a78..00000000000
--- a/app/assets/images/emoji/microscope.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger.png b/app/assets/images/emoji/middle_finger.png
deleted file mode 100644
index 697f7a25eb2..00000000000
--- a/app/assets/images/emoji/middle_finger.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone1.png b/app/assets/images/emoji/middle_finger_tone1.png
deleted file mode 100644
index 61ef12a1548..00000000000
--- a/app/assets/images/emoji/middle_finger_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone2.png b/app/assets/images/emoji/middle_finger_tone2.png
deleted file mode 100644
index c31a69be9af..00000000000
--- a/app/assets/images/emoji/middle_finger_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone3.png b/app/assets/images/emoji/middle_finger_tone3.png
deleted file mode 100644
index 73ac216ce63..00000000000
--- a/app/assets/images/emoji/middle_finger_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone4.png b/app/assets/images/emoji/middle_finger_tone4.png
deleted file mode 100644
index 80b8ab7706d..00000000000
--- a/app/assets/images/emoji/middle_finger_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone5.png b/app/assets/images/emoji/middle_finger_tone5.png
deleted file mode 100644
index a8826b196e8..00000000000
--- a/app/assets/images/emoji/middle_finger_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/military_medal.png b/app/assets/images/emoji/military_medal.png
deleted file mode 100644
index ecd3fb03584..00000000000
--- a/app/assets/images/emoji/military_medal.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/milk.png b/app/assets/images/emoji/milk.png
deleted file mode 100644
index e4fcf2e64f3..00000000000
--- a/app/assets/images/emoji/milk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/milky_way.png b/app/assets/images/emoji/milky_way.png
deleted file mode 100644
index b2b8ac59c5e..00000000000
--- a/app/assets/images/emoji/milky_way.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/minibus.png b/app/assets/images/emoji/minibus.png
deleted file mode 100644
index c60dd8f47ab..00000000000
--- a/app/assets/images/emoji/minibus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/minidisc.png b/app/assets/images/emoji/minidisc.png
deleted file mode 100644
index 9fa94cfbe74..00000000000
--- a/app/assets/images/emoji/minidisc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mobile_phone_off.png b/app/assets/images/emoji/mobile_phone_off.png
deleted file mode 100644
index 8b661ec1c94..00000000000
--- a/app/assets/images/emoji/mobile_phone_off.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/money_mouth.png b/app/assets/images/emoji/money_mouth.png
deleted file mode 100644
index 75fd1e90cb0..00000000000
--- a/app/assets/images/emoji/money_mouth.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/money_with_wings.png b/app/assets/images/emoji/money_with_wings.png
deleted file mode 100644
index f022b04b3c2..00000000000
--- a/app/assets/images/emoji/money_with_wings.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/moneybag.png b/app/assets/images/emoji/moneybag.png
deleted file mode 100644
index b9296be0902..00000000000
--- a/app/assets/images/emoji/moneybag.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/monkey.png b/app/assets/images/emoji/monkey.png
deleted file mode 100644
index 9fae29448e3..00000000000
--- a/app/assets/images/emoji/monkey.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/monkey_face.png b/app/assets/images/emoji/monkey_face.png
deleted file mode 100644
index 7cab9b91a82..00000000000
--- a/app/assets/images/emoji/monkey_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/monorail.png b/app/assets/images/emoji/monorail.png
deleted file mode 100644
index 11eb1f574bf..00000000000
--- a/app/assets/images/emoji/monorail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mortar_board.png b/app/assets/images/emoji/mortar_board.png
deleted file mode 100644
index 8b17ddd9d00..00000000000
--- a/app/assets/images/emoji/mortar_board.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mosque.png b/app/assets/images/emoji/mosque.png
deleted file mode 100644
index ef770b26d96..00000000000
--- a/app/assets/images/emoji/mosque.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/motor_scooter.png b/app/assets/images/emoji/motor_scooter.png
deleted file mode 100644
index c5afa72d807..00000000000
--- a/app/assets/images/emoji/motor_scooter.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/motorboat.png b/app/assets/images/emoji/motorboat.png
deleted file mode 100644
index 0506db1a40f..00000000000
--- a/app/assets/images/emoji/motorboat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/motorcycle.png b/app/assets/images/emoji/motorcycle.png
deleted file mode 100644
index 3d1d567e8ec..00000000000
--- a/app/assets/images/emoji/motorcycle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/motorway.png b/app/assets/images/emoji/motorway.png
deleted file mode 100644
index 8c3d3d03e3f..00000000000
--- a/app/assets/images/emoji/motorway.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mount_fuji.png b/app/assets/images/emoji/mount_fuji.png
deleted file mode 100644
index 88a54752458..00000000000
--- a/app/assets/images/emoji/mount_fuji.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain.png b/app/assets/images/emoji/mountain.png
deleted file mode 100644
index 6722ebdd294..00000000000
--- a/app/assets/images/emoji/mountain.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist.png b/app/assets/images/emoji/mountain_bicyclist.png
deleted file mode 100644
index 41d3dc3ac6f..00000000000
--- a/app/assets/images/emoji/mountain_bicyclist.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone1.png b/app/assets/images/emoji/mountain_bicyclist_tone1.png
deleted file mode 100644
index e9f1daf5e40..00000000000
--- a/app/assets/images/emoji/mountain_bicyclist_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone2.png b/app/assets/images/emoji/mountain_bicyclist_tone2.png
deleted file mode 100644
index 555b9e29d4d..00000000000
--- a/app/assets/images/emoji/mountain_bicyclist_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone3.png b/app/assets/images/emoji/mountain_bicyclist_tone3.png
deleted file mode 100644
index 7df5508ec8c..00000000000
--- a/app/assets/images/emoji/mountain_bicyclist_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone4.png b/app/assets/images/emoji/mountain_bicyclist_tone4.png
deleted file mode 100644
index f94b3450697..00000000000
--- a/app/assets/images/emoji/mountain_bicyclist_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone5.png b/app/assets/images/emoji/mountain_bicyclist_tone5.png
deleted file mode 100644
index 16a45861e1f..00000000000
--- a/app/assets/images/emoji/mountain_bicyclist_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_cableway.png b/app/assets/images/emoji/mountain_cableway.png
deleted file mode 100644
index 1dea73ca53b..00000000000
--- a/app/assets/images/emoji/mountain_cableway.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_railway.png b/app/assets/images/emoji/mountain_railway.png
deleted file mode 100644
index ade2218e469..00000000000
--- a/app/assets/images/emoji/mountain_railway.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_snow.png b/app/assets/images/emoji/mountain_snow.png
deleted file mode 100644
index 76e1cfd8313..00000000000
--- a/app/assets/images/emoji/mountain_snow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mouse.png b/app/assets/images/emoji/mouse.png
deleted file mode 100644
index 50afcd3262e..00000000000
--- a/app/assets/images/emoji/mouse.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mouse2.png b/app/assets/images/emoji/mouse2.png
deleted file mode 100644
index 20fb041f09f..00000000000
--- a/app/assets/images/emoji/mouse2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mouse_three_button.png b/app/assets/images/emoji/mouse_three_button.png
deleted file mode 100644
index e84e96ff6e8..00000000000
--- a/app/assets/images/emoji/mouse_three_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/movie_camera.png b/app/assets/images/emoji/movie_camera.png
deleted file mode 100644
index 4e73b130155..00000000000
--- a/app/assets/images/emoji/movie_camera.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/moyai.png b/app/assets/images/emoji/moyai.png
deleted file mode 100644
index e6a7779c45b..00000000000
--- a/app/assets/images/emoji/moyai.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus.png b/app/assets/images/emoji/mrs_claus.png
deleted file mode 100644
index 9cf2458df1a..00000000000
--- a/app/assets/images/emoji/mrs_claus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone1.png b/app/assets/images/emoji/mrs_claus_tone1.png
deleted file mode 100644
index d8a695d7035..00000000000
--- a/app/assets/images/emoji/mrs_claus_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone2.png b/app/assets/images/emoji/mrs_claus_tone2.png
deleted file mode 100644
index 0e17e8c51f3..00000000000
--- a/app/assets/images/emoji/mrs_claus_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone3.png b/app/assets/images/emoji/mrs_claus_tone3.png
deleted file mode 100644
index c3ee4d1dfae..00000000000
--- a/app/assets/images/emoji/mrs_claus_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone4.png b/app/assets/images/emoji/mrs_claus_tone4.png
deleted file mode 100644
index 68a556da2fe..00000000000
--- a/app/assets/images/emoji/mrs_claus_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone5.png b/app/assets/images/emoji/mrs_claus_tone5.png
deleted file mode 100644
index ccab3c40ff2..00000000000
--- a/app/assets/images/emoji/mrs_claus_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/muscle.png b/app/assets/images/emoji/muscle.png
deleted file mode 100644
index 7e67c1880f7..00000000000
--- a/app/assets/images/emoji/muscle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone1.png b/app/assets/images/emoji/muscle_tone1.png
deleted file mode 100644
index 1522942ce51..00000000000
--- a/app/assets/images/emoji/muscle_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone2.png b/app/assets/images/emoji/muscle_tone2.png
deleted file mode 100644
index 569c6e832ca..00000000000
--- a/app/assets/images/emoji/muscle_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone3.png b/app/assets/images/emoji/muscle_tone3.png
deleted file mode 100644
index 0a76b00fa89..00000000000
--- a/app/assets/images/emoji/muscle_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone4.png b/app/assets/images/emoji/muscle_tone4.png
deleted file mode 100644
index f0cf31328e0..00000000000
--- a/app/assets/images/emoji/muscle_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone5.png b/app/assets/images/emoji/muscle_tone5.png
deleted file mode 100644
index 4fda92460e8..00000000000
--- a/app/assets/images/emoji/muscle_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mushroom.png b/app/assets/images/emoji/mushroom.png
deleted file mode 100644
index dd85742ba2c..00000000000
--- a/app/assets/images/emoji/mushroom.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/musical_keyboard.png b/app/assets/images/emoji/musical_keyboard.png
deleted file mode 100644
index 442b7456842..00000000000
--- a/app/assets/images/emoji/musical_keyboard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/musical_note.png b/app/assets/images/emoji/musical_note.png
deleted file mode 100644
index 06691ef61bb..00000000000
--- a/app/assets/images/emoji/musical_note.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/musical_score.png b/app/assets/images/emoji/musical_score.png
deleted file mode 100644
index 47dc05a8ef5..00000000000
--- a/app/assets/images/emoji/musical_score.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mute.png b/app/assets/images/emoji/mute.png
deleted file mode 100644
index 7c1788e5075..00000000000
--- a/app/assets/images/emoji/mute.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nail_care.png b/app/assets/images/emoji/nail_care.png
deleted file mode 100644
index aa52af7050d..00000000000
--- a/app/assets/images/emoji/nail_care.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone1.png b/app/assets/images/emoji/nail_care_tone1.png
deleted file mode 100644
index 26e883dd244..00000000000
--- a/app/assets/images/emoji/nail_care_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone2.png b/app/assets/images/emoji/nail_care_tone2.png
deleted file mode 100644
index 61257b47ea3..00000000000
--- a/app/assets/images/emoji/nail_care_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone3.png b/app/assets/images/emoji/nail_care_tone3.png
deleted file mode 100644
index 29871b05f62..00000000000
--- a/app/assets/images/emoji/nail_care_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone4.png b/app/assets/images/emoji/nail_care_tone4.png
deleted file mode 100644
index 2881de0b17d..00000000000
--- a/app/assets/images/emoji/nail_care_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone5.png b/app/assets/images/emoji/nail_care_tone5.png
deleted file mode 100644
index a0b7c0a45a6..00000000000
--- a/app/assets/images/emoji/nail_care_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/name_badge.png b/app/assets/images/emoji/name_badge.png
deleted file mode 100644
index ec5ee213e20..00000000000
--- a/app/assets/images/emoji/name_badge.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nauseated_face.png b/app/assets/images/emoji/nauseated_face.png
deleted file mode 100644
index a566c109c28..00000000000
--- a/app/assets/images/emoji/nauseated_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/necktie.png b/app/assets/images/emoji/necktie.png
deleted file mode 100644
index 1804e7f3ff3..00000000000
--- a/app/assets/images/emoji/necktie.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/negative_squared_cross_mark.png b/app/assets/images/emoji/negative_squared_cross_mark.png
deleted file mode 100644
index dae487f1f98..00000000000
--- a/app/assets/images/emoji/negative_squared_cross_mark.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nerd.png b/app/assets/images/emoji/nerd.png
deleted file mode 100644
index 7820bd581dc..00000000000
--- a/app/assets/images/emoji/nerd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/neutral_face.png b/app/assets/images/emoji/neutral_face.png
deleted file mode 100644
index 065d193afe4..00000000000
--- a/app/assets/images/emoji/neutral_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/new.png b/app/assets/images/emoji/new.png
deleted file mode 100644
index b4f85488d1a..00000000000
--- a/app/assets/images/emoji/new.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/new_moon.png b/app/assets/images/emoji/new_moon.png
deleted file mode 100644
index ecff72caa42..00000000000
--- a/app/assets/images/emoji/new_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/new_moon_with_face.png b/app/assets/images/emoji/new_moon_with_face.png
deleted file mode 100644
index 150dd12400c..00000000000
--- a/app/assets/images/emoji/new_moon_with_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/newspaper.png b/app/assets/images/emoji/newspaper.png
deleted file mode 100644
index 2aa8f060bde..00000000000
--- a/app/assets/images/emoji/newspaper.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/newspaper2.png b/app/assets/images/emoji/newspaper2.png
deleted file mode 100644
index f64748df2b2..00000000000
--- a/app/assets/images/emoji/newspaper2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ng.png b/app/assets/images/emoji/ng.png
deleted file mode 100644
index ee8d20f5ebc..00000000000
--- a/app/assets/images/emoji/ng.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/night_with_stars.png b/app/assets/images/emoji/night_with_stars.png
deleted file mode 100644
index ca2018f456d..00000000000
--- a/app/assets/images/emoji/night_with_stars.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nine.png b/app/assets/images/emoji/nine.png
deleted file mode 100644
index 9fce3d1eca9..00000000000
--- a/app/assets/images/emoji/nine.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_bell.png b/app/assets/images/emoji/no_bell.png
deleted file mode 100644
index 15cb38dd1e7..00000000000
--- a/app/assets/images/emoji/no_bell.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_bicycles.png b/app/assets/images/emoji/no_bicycles.png
deleted file mode 100644
index 19c85421ce9..00000000000
--- a/app/assets/images/emoji/no_bicycles.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_entry.png b/app/assets/images/emoji/no_entry.png
deleted file mode 100644
index 476800fc5c6..00000000000
--- a/app/assets/images/emoji/no_entry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_entry_sign.png b/app/assets/images/emoji/no_entry_sign.png
deleted file mode 100644
index d2efd65e74b..00000000000
--- a/app/assets/images/emoji/no_entry_sign.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_good.png b/app/assets/images/emoji/no_good.png
deleted file mode 100644
index ed577100322..00000000000
--- a/app/assets/images/emoji/no_good.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone1.png b/app/assets/images/emoji/no_good_tone1.png
deleted file mode 100644
index 5c1a3cbb884..00000000000
--- a/app/assets/images/emoji/no_good_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone2.png b/app/assets/images/emoji/no_good_tone2.png
deleted file mode 100644
index 80d8021f8fe..00000000000
--- a/app/assets/images/emoji/no_good_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone3.png b/app/assets/images/emoji/no_good_tone3.png
deleted file mode 100644
index 635e6a00815..00000000000
--- a/app/assets/images/emoji/no_good_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone4.png b/app/assets/images/emoji/no_good_tone4.png
deleted file mode 100644
index b96e412a374..00000000000
--- a/app/assets/images/emoji/no_good_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone5.png b/app/assets/images/emoji/no_good_tone5.png
deleted file mode 100644
index 9a7084afa0a..00000000000
--- a/app/assets/images/emoji/no_good_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_mobile_phones.png b/app/assets/images/emoji/no_mobile_phones.png
deleted file mode 100644
index 7b1ae6ea579..00000000000
--- a/app/assets/images/emoji/no_mobile_phones.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_mouth.png b/app/assets/images/emoji/no_mouth.png
deleted file mode 100644
index b642f6c1172..00000000000
--- a/app/assets/images/emoji/no_mouth.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_pedestrians.png b/app/assets/images/emoji/no_pedestrians.png
deleted file mode 100644
index 286aa577a23..00000000000
--- a/app/assets/images/emoji/no_pedestrians.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_smoking.png b/app/assets/images/emoji/no_smoking.png
deleted file mode 100644
index 586b8d29d05..00000000000
--- a/app/assets/images/emoji/no_smoking.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/non-potable_water.png b/app/assets/images/emoji/non-potable_water.png
deleted file mode 100644
index 827d4193f4e..00000000000
--- a/app/assets/images/emoji/non-potable_water.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nose.png b/app/assets/images/emoji/nose.png
deleted file mode 100644
index 2f04ac5f98f..00000000000
--- a/app/assets/images/emoji/nose.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone1.png b/app/assets/images/emoji/nose_tone1.png
deleted file mode 100644
index 8008d17506e..00000000000
--- a/app/assets/images/emoji/nose_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone2.png b/app/assets/images/emoji/nose_tone2.png
deleted file mode 100644
index ac17f26e827..00000000000
--- a/app/assets/images/emoji/nose_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone3.png b/app/assets/images/emoji/nose_tone3.png
deleted file mode 100644
index d8b6cbe0f8e..00000000000
--- a/app/assets/images/emoji/nose_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone4.png b/app/assets/images/emoji/nose_tone4.png
deleted file mode 100644
index 004b2631e2e..00000000000
--- a/app/assets/images/emoji/nose_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone5.png b/app/assets/images/emoji/nose_tone5.png
deleted file mode 100644
index 7b33821f6c9..00000000000
--- a/app/assets/images/emoji/nose_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/notebook.png b/app/assets/images/emoji/notebook.png
deleted file mode 100644
index f6c28b4915d..00000000000
--- a/app/assets/images/emoji/notebook.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/notebook_with_decorative_cover.png b/app/assets/images/emoji/notebook_with_decorative_cover.png
deleted file mode 100644
index 03f566b6d2c..00000000000
--- a/app/assets/images/emoji/notebook_with_decorative_cover.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/notepad_spiral.png b/app/assets/images/emoji/notepad_spiral.png
deleted file mode 100644
index 85faa10d8ea..00000000000
--- a/app/assets/images/emoji/notepad_spiral.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/notes.png b/app/assets/images/emoji/notes.png
deleted file mode 100644
index 57d499aa181..00000000000
--- a/app/assets/images/emoji/notes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nut_and_bolt.png b/app/assets/images/emoji/nut_and_bolt.png
deleted file mode 100644
index 4b9ae155319..00000000000
--- a/app/assets/images/emoji/nut_and_bolt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/o.png b/app/assets/images/emoji/o.png
deleted file mode 100644
index 3fe75ce4675..00000000000
--- a/app/assets/images/emoji/o.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/o2.png b/app/assets/images/emoji/o2.png
deleted file mode 100644
index 73278ba194a..00000000000
--- a/app/assets/images/emoji/o2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ocean.png b/app/assets/images/emoji/ocean.png
deleted file mode 100644
index 45ff1e87703..00000000000
--- a/app/assets/images/emoji/ocean.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/octagonal_sign.png b/app/assets/images/emoji/octagonal_sign.png
deleted file mode 100644
index 5ed61004045..00000000000
--- a/app/assets/images/emoji/octagonal_sign.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/octopus.png b/app/assets/images/emoji/octopus.png
deleted file mode 100644
index 72c84074aac..00000000000
--- a/app/assets/images/emoji/octopus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/oden.png b/app/assets/images/emoji/oden.png
deleted file mode 100644
index d38a849fece..00000000000
--- a/app/assets/images/emoji/oden.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/office.png b/app/assets/images/emoji/office.png
deleted file mode 100644
index 7eee927d1b0..00000000000
--- a/app/assets/images/emoji/office.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/oil.png b/app/assets/images/emoji/oil.png
deleted file mode 100644
index c4c4d42da8b..00000000000
--- a/app/assets/images/emoji/oil.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok.png b/app/assets/images/emoji/ok.png
deleted file mode 100644
index d0d775532ff..00000000000
--- a/app/assets/images/emoji/ok.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand.png b/app/assets/images/emoji/ok_hand.png
deleted file mode 100644
index 028d69b0de3..00000000000
--- a/app/assets/images/emoji/ok_hand.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone1.png b/app/assets/images/emoji/ok_hand_tone1.png
deleted file mode 100644
index cecf7b2ab5a..00000000000
--- a/app/assets/images/emoji/ok_hand_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone2.png b/app/assets/images/emoji/ok_hand_tone2.png
deleted file mode 100644
index c19239bcd3d..00000000000
--- a/app/assets/images/emoji/ok_hand_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone3.png b/app/assets/images/emoji/ok_hand_tone3.png
deleted file mode 100644
index 94b65b03ecd..00000000000
--- a/app/assets/images/emoji/ok_hand_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone4.png b/app/assets/images/emoji/ok_hand_tone4.png
deleted file mode 100644
index 03d26f08e6a..00000000000
--- a/app/assets/images/emoji/ok_hand_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone5.png b/app/assets/images/emoji/ok_hand_tone5.png
deleted file mode 100644
index d4b24086364..00000000000
--- a/app/assets/images/emoji/ok_hand_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman.png b/app/assets/images/emoji/ok_woman.png
deleted file mode 100644
index 90a2c7469c4..00000000000
--- a/app/assets/images/emoji/ok_woman.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone1.png b/app/assets/images/emoji/ok_woman_tone1.png
deleted file mode 100644
index c99543e785b..00000000000
--- a/app/assets/images/emoji/ok_woman_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone2.png b/app/assets/images/emoji/ok_woman_tone2.png
deleted file mode 100644
index ad5fae813db..00000000000
--- a/app/assets/images/emoji/ok_woman_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone3.png b/app/assets/images/emoji/ok_woman_tone3.png
deleted file mode 100644
index 51bf4fab406..00000000000
--- a/app/assets/images/emoji/ok_woman_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone4.png b/app/assets/images/emoji/ok_woman_tone4.png
deleted file mode 100644
index ee3f9dc640a..00000000000
--- a/app/assets/images/emoji/ok_woman_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone5.png b/app/assets/images/emoji/ok_woman_tone5.png
deleted file mode 100644
index 62a9d9237f7..00000000000
--- a/app/assets/images/emoji/ok_woman_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_man.png b/app/assets/images/emoji/older_man.png
deleted file mode 100644
index 4ace4e6f308..00000000000
--- a/app/assets/images/emoji/older_man.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone1.png b/app/assets/images/emoji/older_man_tone1.png
deleted file mode 100644
index ab459baace8..00000000000
--- a/app/assets/images/emoji/older_man_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone2.png b/app/assets/images/emoji/older_man_tone2.png
deleted file mode 100644
index f4dfc7694ea..00000000000
--- a/app/assets/images/emoji/older_man_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone3.png b/app/assets/images/emoji/older_man_tone3.png
deleted file mode 100644
index 5ffd11792f4..00000000000
--- a/app/assets/images/emoji/older_man_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone4.png b/app/assets/images/emoji/older_man_tone4.png
deleted file mode 100644
index b350a764bfd..00000000000
--- a/app/assets/images/emoji/older_man_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone5.png b/app/assets/images/emoji/older_man_tone5.png
deleted file mode 100644
index 05fe24a1708..00000000000
--- a/app/assets/images/emoji/older_man_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_woman.png b/app/assets/images/emoji/older_woman.png
deleted file mode 100644
index 52dc4987143..00000000000
--- a/app/assets/images/emoji/older_woman.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone1.png b/app/assets/images/emoji/older_woman_tone1.png
deleted file mode 100644
index b49e821402c..00000000000
--- a/app/assets/images/emoji/older_woman_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone2.png b/app/assets/images/emoji/older_woman_tone2.png
deleted file mode 100644
index e86bf5ab3b7..00000000000
--- a/app/assets/images/emoji/older_woman_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone3.png b/app/assets/images/emoji/older_woman_tone3.png
deleted file mode 100644
index 83fc14b0874..00000000000
--- a/app/assets/images/emoji/older_woman_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone4.png b/app/assets/images/emoji/older_woman_tone4.png
deleted file mode 100644
index e4aa8a424d4..00000000000
--- a/app/assets/images/emoji/older_woman_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone5.png b/app/assets/images/emoji/older_woman_tone5.png
deleted file mode 100644
index 4009012bb0a..00000000000
--- a/app/assets/images/emoji/older_woman_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/om_symbol.png b/app/assets/images/emoji/om_symbol.png
deleted file mode 100644
index a35c63c459c..00000000000
--- a/app/assets/images/emoji/om_symbol.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/on.png b/app/assets/images/emoji/on.png
deleted file mode 100644
index a0c371ae21e..00000000000
--- a/app/assets/images/emoji/on.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/oncoming_automobile.png b/app/assets/images/emoji/oncoming_automobile.png
deleted file mode 100644
index 3c7e1d52e63..00000000000
--- a/app/assets/images/emoji/oncoming_automobile.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/oncoming_bus.png b/app/assets/images/emoji/oncoming_bus.png
deleted file mode 100644
index ad91e256c7f..00000000000
--- a/app/assets/images/emoji/oncoming_bus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/oncoming_police_car.png b/app/assets/images/emoji/oncoming_police_car.png
deleted file mode 100644
index c9109c85b5d..00000000000
--- a/app/assets/images/emoji/oncoming_police_car.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/oncoming_taxi.png b/app/assets/images/emoji/oncoming_taxi.png
deleted file mode 100644
index fea14e45846..00000000000
--- a/app/assets/images/emoji/oncoming_taxi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/one.png b/app/assets/images/emoji/one.png
deleted file mode 100644
index e6d84b80128..00000000000
--- a/app/assets/images/emoji/one.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_file_folder.png b/app/assets/images/emoji/open_file_folder.png
deleted file mode 100644
index 3993b09222f..00000000000
--- a/app/assets/images/emoji/open_file_folder.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_hands.png b/app/assets/images/emoji/open_hands.png
deleted file mode 100644
index 1cf75c9101e..00000000000
--- a/app/assets/images/emoji/open_hands.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone1.png b/app/assets/images/emoji/open_hands_tone1.png
deleted file mode 100644
index 352d2614f11..00000000000
--- a/app/assets/images/emoji/open_hands_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone2.png b/app/assets/images/emoji/open_hands_tone2.png
deleted file mode 100644
index 70824a50c73..00000000000
--- a/app/assets/images/emoji/open_hands_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone3.png b/app/assets/images/emoji/open_hands_tone3.png
deleted file mode 100644
index d7d136bd3db..00000000000
--- a/app/assets/images/emoji/open_hands_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone4.png b/app/assets/images/emoji/open_hands_tone4.png
deleted file mode 100644
index df4eaa711e7..00000000000
--- a/app/assets/images/emoji/open_hands_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone5.png b/app/assets/images/emoji/open_hands_tone5.png
deleted file mode 100644
index 7dc04eaebd8..00000000000
--- a/app/assets/images/emoji/open_hands_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_mouth.png b/app/assets/images/emoji/open_mouth.png
deleted file mode 100644
index a62cd27e148..00000000000
--- a/app/assets/images/emoji/open_mouth.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ophiuchus.png b/app/assets/images/emoji/ophiuchus.png
deleted file mode 100644
index 0a780a700da..00000000000
--- a/app/assets/images/emoji/ophiuchus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/orange_book.png b/app/assets/images/emoji/orange_book.png
deleted file mode 100644
index ab40e6ae6a2..00000000000
--- a/app/assets/images/emoji/orange_book.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/orthodox_cross.png b/app/assets/images/emoji/orthodox_cross.png
deleted file mode 100644
index 0530e33a4d4..00000000000
--- a/app/assets/images/emoji/orthodox_cross.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/outbox_tray.png b/app/assets/images/emoji/outbox_tray.png
deleted file mode 100644
index 46493ed5b2c..00000000000
--- a/app/assets/images/emoji/outbox_tray.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/owl.png b/app/assets/images/emoji/owl.png
deleted file mode 100644
index fa6815480c3..00000000000
--- a/app/assets/images/emoji/owl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ox.png b/app/assets/images/emoji/ox.png
deleted file mode 100644
index badf5708f2f..00000000000
--- a/app/assets/images/emoji/ox.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/package.png b/app/assets/images/emoji/package.png
deleted file mode 100644
index 85431756ad8..00000000000
--- a/app/assets/images/emoji/package.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/page_facing_up.png b/app/assets/images/emoji/page_facing_up.png
deleted file mode 100644
index ba4ed757e01..00000000000
--- a/app/assets/images/emoji/page_facing_up.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/page_with_curl.png b/app/assets/images/emoji/page_with_curl.png
deleted file mode 100644
index 06355319c74..00000000000
--- a/app/assets/images/emoji/page_with_curl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pager.png b/app/assets/images/emoji/pager.png
deleted file mode 100644
index b24b99306a2..00000000000
--- a/app/assets/images/emoji/pager.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/paintbrush.png b/app/assets/images/emoji/paintbrush.png
deleted file mode 100644
index 28bffbaa3c9..00000000000
--- a/app/assets/images/emoji/paintbrush.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/palm_tree.png b/app/assets/images/emoji/palm_tree.png
deleted file mode 100644
index 4bbb10f4f19..00000000000
--- a/app/assets/images/emoji/palm_tree.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pancakes.png b/app/assets/images/emoji/pancakes.png
deleted file mode 100644
index 6223d1a28e9..00000000000
--- a/app/assets/images/emoji/pancakes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/panda_face.png b/app/assets/images/emoji/panda_face.png
deleted file mode 100644
index 978382775ce..00000000000
--- a/app/assets/images/emoji/panda_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/paperclip.png b/app/assets/images/emoji/paperclip.png
deleted file mode 100644
index 8cd8d4f8750..00000000000
--- a/app/assets/images/emoji/paperclip.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/paperclips.png b/app/assets/images/emoji/paperclips.png
deleted file mode 100644
index 76021e8c705..00000000000
--- a/app/assets/images/emoji/paperclips.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/park.png b/app/assets/images/emoji/park.png
deleted file mode 100644
index 63ec7016301..00000000000
--- a/app/assets/images/emoji/park.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/parking.png b/app/assets/images/emoji/parking.png
deleted file mode 100644
index 7be7dac27e8..00000000000
--- a/app/assets/images/emoji/parking.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/part_alternation_mark.png b/app/assets/images/emoji/part_alternation_mark.png
deleted file mode 100644
index 70453d41528..00000000000
--- a/app/assets/images/emoji/part_alternation_mark.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/partly_sunny.png b/app/assets/images/emoji/partly_sunny.png
deleted file mode 100644
index a55e59c344c..00000000000
--- a/app/assets/images/emoji/partly_sunny.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/passport_control.png b/app/assets/images/emoji/passport_control.png
deleted file mode 100644
index 079e34ee4d4..00000000000
--- a/app/assets/images/emoji/passport_control.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pause_button.png b/app/assets/images/emoji/pause_button.png
deleted file mode 100644
index 4f07e7ebfd7..00000000000
--- a/app/assets/images/emoji/pause_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/peace.png b/app/assets/images/emoji/peace.png
deleted file mode 100644
index 86033faf477..00000000000
--- a/app/assets/images/emoji/peace.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/peach.png b/app/assets/images/emoji/peach.png
deleted file mode 100644
index 9ab57cbb758..00000000000
--- a/app/assets/images/emoji/peach.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/peanuts.png b/app/assets/images/emoji/peanuts.png
deleted file mode 100644
index b64fadad010..00000000000
--- a/app/assets/images/emoji/peanuts.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pear.png b/app/assets/images/emoji/pear.png
deleted file mode 100644
index 3869f718bcf..00000000000
--- a/app/assets/images/emoji/pear.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pen_ballpoint.png b/app/assets/images/emoji/pen_ballpoint.png
deleted file mode 100644
index 6ef7a342433..00000000000
--- a/app/assets/images/emoji/pen_ballpoint.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pen_fountain.png b/app/assets/images/emoji/pen_fountain.png
deleted file mode 100644
index 3ca4bd2c231..00000000000
--- a/app/assets/images/emoji/pen_fountain.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pencil.png b/app/assets/images/emoji/pencil.png
deleted file mode 100644
index edc6155e168..00000000000
--- a/app/assets/images/emoji/pencil.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pencil2.png b/app/assets/images/emoji/pencil2.png
deleted file mode 100644
index 3833d590fa2..00000000000
--- a/app/assets/images/emoji/pencil2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/penguin.png b/app/assets/images/emoji/penguin.png
deleted file mode 100644
index c0064fb9734..00000000000
--- a/app/assets/images/emoji/penguin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pensive.png b/app/assets/images/emoji/pensive.png
deleted file mode 100644
index 490fb566954..00000000000
--- a/app/assets/images/emoji/pensive.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/performing_arts.png b/app/assets/images/emoji/performing_arts.png
deleted file mode 100644
index 685441fdaa1..00000000000
--- a/app/assets/images/emoji/performing_arts.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/persevere.png b/app/assets/images/emoji/persevere.png
deleted file mode 100644
index 646a05fe908..00000000000
--- a/app/assets/images/emoji/persevere.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning.png b/app/assets/images/emoji/person_frowning.png
deleted file mode 100644
index 579324959a1..00000000000
--- a/app/assets/images/emoji/person_frowning.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone1.png b/app/assets/images/emoji/person_frowning_tone1.png
deleted file mode 100644
index 21d3bb43923..00000000000
--- a/app/assets/images/emoji/person_frowning_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone2.png b/app/assets/images/emoji/person_frowning_tone2.png
deleted file mode 100644
index 973f5fc8382..00000000000
--- a/app/assets/images/emoji/person_frowning_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone3.png b/app/assets/images/emoji/person_frowning_tone3.png
deleted file mode 100644
index 41fbcc78816..00000000000
--- a/app/assets/images/emoji/person_frowning_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone4.png b/app/assets/images/emoji/person_frowning_tone4.png
deleted file mode 100644
index 5a37c741030..00000000000
--- a/app/assets/images/emoji/person_frowning_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone5.png b/app/assets/images/emoji/person_frowning_tone5.png
deleted file mode 100644
index e08141f3efe..00000000000
--- a/app/assets/images/emoji/person_frowning_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair.png b/app/assets/images/emoji/person_with_blond_hair.png
deleted file mode 100644
index ad6f01a7dda..00000000000
--- a/app/assets/images/emoji/person_with_blond_hair.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone1.png b/app/assets/images/emoji/person_with_blond_hair_tone1.png
deleted file mode 100644
index 7d18ef24445..00000000000
--- a/app/assets/images/emoji/person_with_blond_hair_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone2.png b/app/assets/images/emoji/person_with_blond_hair_tone2.png
deleted file mode 100644
index dae1307315c..00000000000
--- a/app/assets/images/emoji/person_with_blond_hair_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone3.png b/app/assets/images/emoji/person_with_blond_hair_tone3.png
deleted file mode 100644
index 684677e8e5a..00000000000
--- a/app/assets/images/emoji/person_with_blond_hair_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone4.png b/app/assets/images/emoji/person_with_blond_hair_tone4.png
deleted file mode 100644
index 012be0b51f8..00000000000
--- a/app/assets/images/emoji/person_with_blond_hair_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone5.png b/app/assets/images/emoji/person_with_blond_hair_tone5.png
deleted file mode 100644
index d4ecc4cf44b..00000000000
--- a/app/assets/images/emoji/person_with_blond_hair_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face.png b/app/assets/images/emoji/person_with_pouting_face.png
deleted file mode 100644
index 10eb0571078..00000000000
--- a/app/assets/images/emoji/person_with_pouting_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone1.png b/app/assets/images/emoji/person_with_pouting_face_tone1.png
deleted file mode 100644
index 57e826b75a4..00000000000
--- a/app/assets/images/emoji/person_with_pouting_face_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone2.png b/app/assets/images/emoji/person_with_pouting_face_tone2.png
deleted file mode 100644
index 3f317c0c25f..00000000000
--- a/app/assets/images/emoji/person_with_pouting_face_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone3.png b/app/assets/images/emoji/person_with_pouting_face_tone3.png
deleted file mode 100644
index d2fbb6c20bf..00000000000
--- a/app/assets/images/emoji/person_with_pouting_face_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone4.png b/app/assets/images/emoji/person_with_pouting_face_tone4.png
deleted file mode 100644
index 643ceb4a5c5..00000000000
--- a/app/assets/images/emoji/person_with_pouting_face_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone5.png b/app/assets/images/emoji/person_with_pouting_face_tone5.png
deleted file mode 100644
index b2eb6859c32..00000000000
--- a/app/assets/images/emoji/person_with_pouting_face_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pick.png b/app/assets/images/emoji/pick.png
deleted file mode 100644
index 6370fe6d791..00000000000
--- a/app/assets/images/emoji/pick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pig.png b/app/assets/images/emoji/pig.png
deleted file mode 100644
index afe05ca1676..00000000000
--- a/app/assets/images/emoji/pig.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pig2.png b/app/assets/images/emoji/pig2.png
deleted file mode 100644
index 5f31c1a2d75..00000000000
--- a/app/assets/images/emoji/pig2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pig_nose.png b/app/assets/images/emoji/pig_nose.png
deleted file mode 100644
index 3610ae4a910..00000000000
--- a/app/assets/images/emoji/pig_nose.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pill.png b/app/assets/images/emoji/pill.png
deleted file mode 100644
index 1d4530e77a3..00000000000
--- a/app/assets/images/emoji/pill.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pineapple.png b/app/assets/images/emoji/pineapple.png
deleted file mode 100644
index c89a1606462..00000000000
--- a/app/assets/images/emoji/pineapple.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ping_pong.png b/app/assets/images/emoji/ping_pong.png
deleted file mode 100644
index ff3c51727d1..00000000000
--- a/app/assets/images/emoji/ping_pong.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pisces.png b/app/assets/images/emoji/pisces.png
deleted file mode 100644
index 7f6f646a95c..00000000000
--- a/app/assets/images/emoji/pisces.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pizza.png b/app/assets/images/emoji/pizza.png
deleted file mode 100644
index e07365cb398..00000000000
--- a/app/assets/images/emoji/pizza.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/place_of_worship.png b/app/assets/images/emoji/place_of_worship.png
deleted file mode 100644
index 207d59cce85..00000000000
--- a/app/assets/images/emoji/place_of_worship.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/play_pause.png b/app/assets/images/emoji/play_pause.png
deleted file mode 100644
index a9f857139ac..00000000000
--- a/app/assets/images/emoji/play_pause.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_down.png b/app/assets/images/emoji/point_down.png
deleted file mode 100644
index 00d3d13ab5c..00000000000
--- a/app/assets/images/emoji/point_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone1.png b/app/assets/images/emoji/point_down_tone1.png
deleted file mode 100644
index 140f157d8c7..00000000000
--- a/app/assets/images/emoji/point_down_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone2.png b/app/assets/images/emoji/point_down_tone2.png
deleted file mode 100644
index d518544f7fa..00000000000
--- a/app/assets/images/emoji/point_down_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone3.png b/app/assets/images/emoji/point_down_tone3.png
deleted file mode 100644
index 018b688b8b7..00000000000
--- a/app/assets/images/emoji/point_down_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone4.png b/app/assets/images/emoji/point_down_tone4.png
deleted file mode 100644
index 98845bf6f72..00000000000
--- a/app/assets/images/emoji/point_down_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone5.png b/app/assets/images/emoji/point_down_tone5.png
deleted file mode 100644
index 9a9b039a9fc..00000000000
--- a/app/assets/images/emoji/point_down_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_left.png b/app/assets/images/emoji/point_left.png
deleted file mode 100644
index 599fa2e3cf1..00000000000
--- a/app/assets/images/emoji/point_left.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone1.png b/app/assets/images/emoji/point_left_tone1.png
deleted file mode 100644
index 88e2c306076..00000000000
--- a/app/assets/images/emoji/point_left_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone2.png b/app/assets/images/emoji/point_left_tone2.png
deleted file mode 100644
index d3c89d87c5f..00000000000
--- a/app/assets/images/emoji/point_left_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone3.png b/app/assets/images/emoji/point_left_tone3.png
deleted file mode 100644
index b23b9167358..00000000000
--- a/app/assets/images/emoji/point_left_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone4.png b/app/assets/images/emoji/point_left_tone4.png
deleted file mode 100644
index 3093f325c27..00000000000
--- a/app/assets/images/emoji/point_left_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone5.png b/app/assets/images/emoji/point_left_tone5.png
deleted file mode 100644
index 2b4cbfa120c..00000000000
--- a/app/assets/images/emoji/point_left_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_right.png b/app/assets/images/emoji/point_right.png
deleted file mode 100644
index 93a3cd34aa5..00000000000
--- a/app/assets/images/emoji/point_right.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone1.png b/app/assets/images/emoji/point_right_tone1.png
deleted file mode 100644
index 4a28c6bbc89..00000000000
--- a/app/assets/images/emoji/point_right_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone2.png b/app/assets/images/emoji/point_right_tone2.png
deleted file mode 100644
index 7cb13231733..00000000000
--- a/app/assets/images/emoji/point_right_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone3.png b/app/assets/images/emoji/point_right_tone3.png
deleted file mode 100644
index 5514807d71a..00000000000
--- a/app/assets/images/emoji/point_right_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone4.png b/app/assets/images/emoji/point_right_tone4.png
deleted file mode 100644
index b8541d6440d..00000000000
--- a/app/assets/images/emoji/point_right_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone5.png b/app/assets/images/emoji/point_right_tone5.png
deleted file mode 100644
index 1b7aab07bb1..00000000000
--- a/app/assets/images/emoji/point_right_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up.png b/app/assets/images/emoji/point_up.png
deleted file mode 100644
index f4978ff0f00..00000000000
--- a/app/assets/images/emoji/point_up.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2.png b/app/assets/images/emoji/point_up_2.png
deleted file mode 100644
index bc496dfeae4..00000000000
--- a/app/assets/images/emoji/point_up_2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone1.png b/app/assets/images/emoji/point_up_2_tone1.png
deleted file mode 100644
index a12a7e78430..00000000000
--- a/app/assets/images/emoji/point_up_2_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone2.png b/app/assets/images/emoji/point_up_2_tone2.png
deleted file mode 100644
index cdff40ceab0..00000000000
--- a/app/assets/images/emoji/point_up_2_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone3.png b/app/assets/images/emoji/point_up_2_tone3.png
deleted file mode 100644
index a07ce9e5ae8..00000000000
--- a/app/assets/images/emoji/point_up_2_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone4.png b/app/assets/images/emoji/point_up_2_tone4.png
deleted file mode 100644
index 4f86c88ba42..00000000000
--- a/app/assets/images/emoji/point_up_2_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone5.png b/app/assets/images/emoji/point_up_2_tone5.png
deleted file mode 100644
index ed1b26c35d3..00000000000
--- a/app/assets/images/emoji/point_up_2_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone1.png b/app/assets/images/emoji/point_up_tone1.png
deleted file mode 100644
index 6a9db21d64c..00000000000
--- a/app/assets/images/emoji/point_up_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone2.png b/app/assets/images/emoji/point_up_tone2.png
deleted file mode 100644
index 15aa9ea0e05..00000000000
--- a/app/assets/images/emoji/point_up_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone3.png b/app/assets/images/emoji/point_up_tone3.png
deleted file mode 100644
index 652b73a9c5d..00000000000
--- a/app/assets/images/emoji/point_up_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone4.png b/app/assets/images/emoji/point_up_tone4.png
deleted file mode 100644
index 692bad926e9..00000000000
--- a/app/assets/images/emoji/point_up_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone5.png b/app/assets/images/emoji/point_up_tone5.png
deleted file mode 100644
index 1e1b10fb71c..00000000000
--- a/app/assets/images/emoji/point_up_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/police_car.png b/app/assets/images/emoji/police_car.png
deleted file mode 100644
index 3da4253de7e..00000000000
--- a/app/assets/images/emoji/police_car.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/poodle.png b/app/assets/images/emoji/poodle.png
deleted file mode 100644
index 8ec39e396af..00000000000
--- a/app/assets/images/emoji/poodle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/poop.png b/app/assets/images/emoji/poop.png
deleted file mode 100644
index 10b15e72d56..00000000000
--- a/app/assets/images/emoji/poop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/popcorn.png b/app/assets/images/emoji/popcorn.png
deleted file mode 100644
index 36853e381d4..00000000000
--- a/app/assets/images/emoji/popcorn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/post_office.png b/app/assets/images/emoji/post_office.png
deleted file mode 100644
index a23848f9aa0..00000000000
--- a/app/assets/images/emoji/post_office.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/postal_horn.png b/app/assets/images/emoji/postal_horn.png
deleted file mode 100644
index c173b8dbd67..00000000000
--- a/app/assets/images/emoji/postal_horn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/postbox.png b/app/assets/images/emoji/postbox.png
deleted file mode 100644
index 07c9c4ab3d6..00000000000
--- a/app/assets/images/emoji/postbox.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/potable_water.png b/app/assets/images/emoji/potable_water.png
deleted file mode 100644
index 2c610049459..00000000000
--- a/app/assets/images/emoji/potable_water.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/potato.png b/app/assets/images/emoji/potato.png
deleted file mode 100644
index 70350ca2c0a..00000000000
--- a/app/assets/images/emoji/potato.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pouch.png b/app/assets/images/emoji/pouch.png
deleted file mode 100644
index 8795c6c66ff..00000000000
--- a/app/assets/images/emoji/pouch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/poultry_leg.png b/app/assets/images/emoji/poultry_leg.png
deleted file mode 100644
index eea4a53a2f9..00000000000
--- a/app/assets/images/emoji/poultry_leg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pound.png b/app/assets/images/emoji/pound.png
deleted file mode 100644
index a0d4c4099e9..00000000000
--- a/app/assets/images/emoji/pound.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pouting_cat.png b/app/assets/images/emoji/pouting_cat.png
deleted file mode 100644
index 41ddfeab42b..00000000000
--- a/app/assets/images/emoji/pouting_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pray.png b/app/assets/images/emoji/pray.png
deleted file mode 100644
index 8347f2435be..00000000000
--- a/app/assets/images/emoji/pray.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone1.png b/app/assets/images/emoji/pray_tone1.png
deleted file mode 100644
index 060ef257172..00000000000
--- a/app/assets/images/emoji/pray_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone2.png b/app/assets/images/emoji/pray_tone2.png
deleted file mode 100644
index 56dc607c07a..00000000000
--- a/app/assets/images/emoji/pray_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone3.png b/app/assets/images/emoji/pray_tone3.png
deleted file mode 100644
index 0f33b862008..00000000000
--- a/app/assets/images/emoji/pray_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone4.png b/app/assets/images/emoji/pray_tone4.png
deleted file mode 100644
index 2ea8dc11657..00000000000
--- a/app/assets/images/emoji/pray_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone5.png b/app/assets/images/emoji/pray_tone5.png
deleted file mode 100644
index 2128a6c4703..00000000000
--- a/app/assets/images/emoji/pray_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prayer_beads.png b/app/assets/images/emoji/prayer_beads.png
deleted file mode 100644
index a4b6dfcc62e..00000000000
--- a/app/assets/images/emoji/prayer_beads.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman.png b/app/assets/images/emoji/pregnant_woman.png
deleted file mode 100644
index 084e83a414a..00000000000
--- a/app/assets/images/emoji/pregnant_woman.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone1.png b/app/assets/images/emoji/pregnant_woman_tone1.png
deleted file mode 100644
index a78703b33aa..00000000000
--- a/app/assets/images/emoji/pregnant_woman_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone2.png b/app/assets/images/emoji/pregnant_woman_tone2.png
deleted file mode 100644
index 0068c6c4a77..00000000000
--- a/app/assets/images/emoji/pregnant_woman_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone3.png b/app/assets/images/emoji/pregnant_woman_tone3.png
deleted file mode 100644
index 3206296b684..00000000000
--- a/app/assets/images/emoji/pregnant_woman_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone4.png b/app/assets/images/emoji/pregnant_woman_tone4.png
deleted file mode 100644
index 120fda5cd8c..00000000000
--- a/app/assets/images/emoji/pregnant_woman_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone5.png b/app/assets/images/emoji/pregnant_woman_tone5.png
deleted file mode 100644
index 569bfdf05ce..00000000000
--- a/app/assets/images/emoji/pregnant_woman_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prince.png b/app/assets/images/emoji/prince.png
deleted file mode 100644
index 38d69344c84..00000000000
--- a/app/assets/images/emoji/prince.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone1.png b/app/assets/images/emoji/prince_tone1.png
deleted file mode 100644
index 849930c8887..00000000000
--- a/app/assets/images/emoji/prince_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone2.png b/app/assets/images/emoji/prince_tone2.png
deleted file mode 100644
index 23d8b3b1285..00000000000
--- a/app/assets/images/emoji/prince_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone3.png b/app/assets/images/emoji/prince_tone3.png
deleted file mode 100644
index db6dfff0647..00000000000
--- a/app/assets/images/emoji/prince_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone4.png b/app/assets/images/emoji/prince_tone4.png
deleted file mode 100644
index 8e10f8be6a8..00000000000
--- a/app/assets/images/emoji/prince_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone5.png b/app/assets/images/emoji/prince_tone5.png
deleted file mode 100644
index 138d4ea7048..00000000000
--- a/app/assets/images/emoji/prince_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/princess.png b/app/assets/images/emoji/princess.png
deleted file mode 100644
index 879e9fa8c5d..00000000000
--- a/app/assets/images/emoji/princess.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone1.png b/app/assets/images/emoji/princess_tone1.png
deleted file mode 100644
index c28078cdc36..00000000000
--- a/app/assets/images/emoji/princess_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone2.png b/app/assets/images/emoji/princess_tone2.png
deleted file mode 100644
index dcd20e6ecd4..00000000000
--- a/app/assets/images/emoji/princess_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone3.png b/app/assets/images/emoji/princess_tone3.png
deleted file mode 100644
index cde6f315c56..00000000000
--- a/app/assets/images/emoji/princess_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone4.png b/app/assets/images/emoji/princess_tone4.png
deleted file mode 100644
index c71e69caaef..00000000000
--- a/app/assets/images/emoji/princess_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone5.png b/app/assets/images/emoji/princess_tone5.png
deleted file mode 100644
index 063e2645910..00000000000
--- a/app/assets/images/emoji/princess_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/printer.png b/app/assets/images/emoji/printer.png
deleted file mode 100644
index 027c830f0fe..00000000000
--- a/app/assets/images/emoji/printer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/projector.png b/app/assets/images/emoji/projector.png
deleted file mode 100644
index ce9ab0daa28..00000000000
--- a/app/assets/images/emoji/projector.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/punch.png b/app/assets/images/emoji/punch.png
deleted file mode 100644
index b14ca5f5211..00000000000
--- a/app/assets/images/emoji/punch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone1.png b/app/assets/images/emoji/punch_tone1.png
deleted file mode 100644
index 93c7d17fb47..00000000000
--- a/app/assets/images/emoji/punch_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone2.png b/app/assets/images/emoji/punch_tone2.png
deleted file mode 100644
index c0a1af6e10a..00000000000
--- a/app/assets/images/emoji/punch_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone3.png b/app/assets/images/emoji/punch_tone3.png
deleted file mode 100644
index 1458b021201..00000000000
--- a/app/assets/images/emoji/punch_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone4.png b/app/assets/images/emoji/punch_tone4.png
deleted file mode 100644
index c1466bfcdef..00000000000
--- a/app/assets/images/emoji/punch_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone5.png b/app/assets/images/emoji/punch_tone5.png
deleted file mode 100644
index 00b4ddb8953..00000000000
--- a/app/assets/images/emoji/punch_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/purple_heart.png b/app/assets/images/emoji/purple_heart.png
deleted file mode 100644
index 95c53a9ade6..00000000000
--- a/app/assets/images/emoji/purple_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/purse.png b/app/assets/images/emoji/purse.png
deleted file mode 100644
index 981346193c5..00000000000
--- a/app/assets/images/emoji/purse.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pushpin.png b/app/assets/images/emoji/pushpin.png
deleted file mode 100644
index 57e07d7f4cc..00000000000
--- a/app/assets/images/emoji/pushpin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/put_litter_in_its_place.png b/app/assets/images/emoji/put_litter_in_its_place.png
deleted file mode 100644
index 82a84f9a375..00000000000
--- a/app/assets/images/emoji/put_litter_in_its_place.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/question.png b/app/assets/images/emoji/question.png
deleted file mode 100644
index 5a58f3458aa..00000000000
--- a/app/assets/images/emoji/question.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rabbit.png b/app/assets/images/emoji/rabbit.png
deleted file mode 100644
index ea75ab0426e..00000000000
--- a/app/assets/images/emoji/rabbit.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rabbit2.png b/app/assets/images/emoji/rabbit2.png
deleted file mode 100644
index 2c8a29c642f..00000000000
--- a/app/assets/images/emoji/rabbit2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/race_car.png b/app/assets/images/emoji/race_car.png
deleted file mode 100644
index fe3f045f446..00000000000
--- a/app/assets/images/emoji/race_car.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/racehorse.png b/app/assets/images/emoji/racehorse.png
deleted file mode 100644
index b3e73cc8903..00000000000
--- a/app/assets/images/emoji/racehorse.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/radio.png b/app/assets/images/emoji/radio.png
deleted file mode 100644
index dec381fa242..00000000000
--- a/app/assets/images/emoji/radio.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/radio_button.png b/app/assets/images/emoji/radio_button.png
deleted file mode 100644
index 3a23449d917..00000000000
--- a/app/assets/images/emoji/radio_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/radioactive.png b/app/assets/images/emoji/radioactive.png
deleted file mode 100644
index 3b46199fe37..00000000000
--- a/app/assets/images/emoji/radioactive.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rage.png b/app/assets/images/emoji/rage.png
deleted file mode 100644
index 9d739bd40ad..00000000000
--- a/app/assets/images/emoji/rage.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/railway_car.png b/app/assets/images/emoji/railway_car.png
deleted file mode 100644
index a9acbf13008..00000000000
--- a/app/assets/images/emoji/railway_car.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/railway_track.png b/app/assets/images/emoji/railway_track.png
deleted file mode 100644
index e1a7a0d1430..00000000000
--- a/app/assets/images/emoji/railway_track.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rainbow.png b/app/assets/images/emoji/rainbow.png
deleted file mode 100644
index 154735d7147..00000000000
--- a/app/assets/images/emoji/rainbow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand.png b/app/assets/images/emoji/raised_back_of_hand.png
deleted file mode 100644
index 479234294b4..00000000000
--- a/app/assets/images/emoji/raised_back_of_hand.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone1.png b/app/assets/images/emoji/raised_back_of_hand_tone1.png
deleted file mode 100644
index 813d28499b5..00000000000
--- a/app/assets/images/emoji/raised_back_of_hand_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone2.png b/app/assets/images/emoji/raised_back_of_hand_tone2.png
deleted file mode 100644
index 192ff795e37..00000000000
--- a/app/assets/images/emoji/raised_back_of_hand_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone3.png b/app/assets/images/emoji/raised_back_of_hand_tone3.png
deleted file mode 100644
index 61a727abe6b..00000000000
--- a/app/assets/images/emoji/raised_back_of_hand_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone4.png b/app/assets/images/emoji/raised_back_of_hand_tone4.png
deleted file mode 100644
index 2e83da511f5..00000000000
--- a/app/assets/images/emoji/raised_back_of_hand_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone5.png b/app/assets/images/emoji/raised_back_of_hand_tone5.png
deleted file mode 100644
index d7a5b95a02c..00000000000
--- a/app/assets/images/emoji/raised_back_of_hand_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand.png b/app/assets/images/emoji/raised_hand.png
deleted file mode 100644
index 6b2954315d1..00000000000
--- a/app/assets/images/emoji/raised_hand.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone1.png b/app/assets/images/emoji/raised_hand_tone1.png
deleted file mode 100644
index 3b752902c07..00000000000
--- a/app/assets/images/emoji/raised_hand_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone2.png b/app/assets/images/emoji/raised_hand_tone2.png
deleted file mode 100644
index 44e2a514c60..00000000000
--- a/app/assets/images/emoji/raised_hand_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone3.png b/app/assets/images/emoji/raised_hand_tone3.png
deleted file mode 100644
index 5bb62a7528a..00000000000
--- a/app/assets/images/emoji/raised_hand_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone4.png b/app/assets/images/emoji/raised_hand_tone4.png
deleted file mode 100644
index c7f8c9ec270..00000000000
--- a/app/assets/images/emoji/raised_hand_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone5.png b/app/assets/images/emoji/raised_hand_tone5.png
deleted file mode 100644
index c601b58a73e..00000000000
--- a/app/assets/images/emoji/raised_hand_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands.png b/app/assets/images/emoji/raised_hands.png
deleted file mode 100644
index c0155f728e7..00000000000
--- a/app/assets/images/emoji/raised_hands.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone1.png b/app/assets/images/emoji/raised_hands_tone1.png
deleted file mode 100644
index 1168b8236b6..00000000000
--- a/app/assets/images/emoji/raised_hands_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone2.png b/app/assets/images/emoji/raised_hands_tone2.png
deleted file mode 100644
index 322de622903..00000000000
--- a/app/assets/images/emoji/raised_hands_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone3.png b/app/assets/images/emoji/raised_hands_tone3.png
deleted file mode 100644
index 2aa24e05ae1..00000000000
--- a/app/assets/images/emoji/raised_hands_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone4.png b/app/assets/images/emoji/raised_hands_tone4.png
deleted file mode 100644
index f31bf0db992..00000000000
--- a/app/assets/images/emoji/raised_hands_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone5.png b/app/assets/images/emoji/raised_hands_tone5.png
deleted file mode 100644
index 5e95067f98b..00000000000
--- a/app/assets/images/emoji/raised_hands_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand.png b/app/assets/images/emoji/raising_hand.png
deleted file mode 100644
index 2880708c0cc..00000000000
--- a/app/assets/images/emoji/raising_hand.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone1.png b/app/assets/images/emoji/raising_hand_tone1.png
deleted file mode 100644
index 1c90e3e2689..00000000000
--- a/app/assets/images/emoji/raising_hand_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone2.png b/app/assets/images/emoji/raising_hand_tone2.png
deleted file mode 100644
index 82c3ef2bfc5..00000000000
--- a/app/assets/images/emoji/raising_hand_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone3.png b/app/assets/images/emoji/raising_hand_tone3.png
deleted file mode 100644
index 1b1da2aa0ca..00000000000
--- a/app/assets/images/emoji/raising_hand_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone4.png b/app/assets/images/emoji/raising_hand_tone4.png
deleted file mode 100644
index e453855c01f..00000000000
--- a/app/assets/images/emoji/raising_hand_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone5.png b/app/assets/images/emoji/raising_hand_tone5.png
deleted file mode 100644
index b86200fd844..00000000000
--- a/app/assets/images/emoji/raising_hand_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ram.png b/app/assets/images/emoji/ram.png
deleted file mode 100644
index 52a44464c9b..00000000000
--- a/app/assets/images/emoji/ram.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ramen.png b/app/assets/images/emoji/ramen.png
deleted file mode 100644
index c1cb7cd7384..00000000000
--- a/app/assets/images/emoji/ramen.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rat.png b/app/assets/images/emoji/rat.png
deleted file mode 100644
index 86219144f10..00000000000
--- a/app/assets/images/emoji/rat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/record_button.png b/app/assets/images/emoji/record_button.png
deleted file mode 100644
index ada52830fce..00000000000
--- a/app/assets/images/emoji/record_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/recycle.png b/app/assets/images/emoji/recycle.png
deleted file mode 100644
index 9221f095c37..00000000000
--- a/app/assets/images/emoji/recycle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/red_car.png b/app/assets/images/emoji/red_car.png
deleted file mode 100644
index b3e6a774dea..00000000000
--- a/app/assets/images/emoji/red_car.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/red_circle.png b/app/assets/images/emoji/red_circle.png
deleted file mode 100644
index 4bef930d92f..00000000000
--- a/app/assets/images/emoji/red_circle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/registered.png b/app/assets/images/emoji/registered.png
deleted file mode 100644
index 53ef9f2d4e6..00000000000
--- a/app/assets/images/emoji/registered.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/relaxed.png b/app/assets/images/emoji/relaxed.png
deleted file mode 100644
index e9e53c03d45..00000000000
--- a/app/assets/images/emoji/relaxed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/relieved.png b/app/assets/images/emoji/relieved.png
deleted file mode 100644
index 715ad0bf53f..00000000000
--- a/app/assets/images/emoji/relieved.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/reminder_ribbon.png b/app/assets/images/emoji/reminder_ribbon.png
deleted file mode 100644
index 3988bbd094c..00000000000
--- a/app/assets/images/emoji/reminder_ribbon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/repeat.png b/app/assets/images/emoji/repeat.png
deleted file mode 100644
index 540ce4e0fba..00000000000
--- a/app/assets/images/emoji/repeat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/repeat_one.png b/app/assets/images/emoji/repeat_one.png
deleted file mode 100644
index 9567e83337f..00000000000
--- a/app/assets/images/emoji/repeat_one.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/restroom.png b/app/assets/images/emoji/restroom.png
deleted file mode 100644
index 9588e0f0ef7..00000000000
--- a/app/assets/images/emoji/restroom.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/revolving_hearts.png b/app/assets/images/emoji/revolving_hearts.png
deleted file mode 100644
index 7b9d1948f73..00000000000
--- a/app/assets/images/emoji/revolving_hearts.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rewind.png b/app/assets/images/emoji/rewind.png
deleted file mode 100644
index e22e2bd3da5..00000000000
--- a/app/assets/images/emoji/rewind.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rhino.png b/app/assets/images/emoji/rhino.png
deleted file mode 100644
index 12f4e0d9d9b..00000000000
--- a/app/assets/images/emoji/rhino.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ribbon.png b/app/assets/images/emoji/ribbon.png
deleted file mode 100644
index 0f253c3d8c8..00000000000
--- a/app/assets/images/emoji/ribbon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rice.png b/app/assets/images/emoji/rice.png
deleted file mode 100644
index 6e3ac7956b1..00000000000
--- a/app/assets/images/emoji/rice.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rice_ball.png b/app/assets/images/emoji/rice_ball.png
deleted file mode 100644
index d3d8ee25cb8..00000000000
--- a/app/assets/images/emoji/rice_ball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rice_cracker.png b/app/assets/images/emoji/rice_cracker.png
deleted file mode 100644
index 7fbd08e4ff9..00000000000
--- a/app/assets/images/emoji/rice_cracker.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rice_scene.png b/app/assets/images/emoji/rice_scene.png
deleted file mode 100644
index 1a28426592a..00000000000
--- a/app/assets/images/emoji/rice_scene.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist.png b/app/assets/images/emoji/right_facing_fist.png
deleted file mode 100644
index 754ed066d2c..00000000000
--- a/app/assets/images/emoji/right_facing_fist.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone1.png b/app/assets/images/emoji/right_facing_fist_tone1.png
deleted file mode 100644
index 33ded2f61a6..00000000000
--- a/app/assets/images/emoji/right_facing_fist_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone2.png b/app/assets/images/emoji/right_facing_fist_tone2.png
deleted file mode 100644
index 88054e335c7..00000000000
--- a/app/assets/images/emoji/right_facing_fist_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone3.png b/app/assets/images/emoji/right_facing_fist_tone3.png
deleted file mode 100644
index 84b9f5da7f7..00000000000
--- a/app/assets/images/emoji/right_facing_fist_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone4.png b/app/assets/images/emoji/right_facing_fist_tone4.png
deleted file mode 100644
index e741cfea68b..00000000000
--- a/app/assets/images/emoji/right_facing_fist_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone5.png b/app/assets/images/emoji/right_facing_fist_tone5.png
deleted file mode 100644
index cf66d760c1f..00000000000
--- a/app/assets/images/emoji/right_facing_fist_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ring.png b/app/assets/images/emoji/ring.png
deleted file mode 100644
index 87d227adb74..00000000000
--- a/app/assets/images/emoji/ring.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/robot.png b/app/assets/images/emoji/robot.png
deleted file mode 100644
index 7cc62612c6a..00000000000
--- a/app/assets/images/emoji/robot.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rocket.png b/app/assets/images/emoji/rocket.png
deleted file mode 100644
index 0d8da089a37..00000000000
--- a/app/assets/images/emoji/rocket.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rofl.png b/app/assets/images/emoji/rofl.png
deleted file mode 100644
index b1736fedfeb..00000000000
--- a/app/assets/images/emoji/rofl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/roller_coaster.png b/app/assets/images/emoji/roller_coaster.png
deleted file mode 100644
index 5b849e071e8..00000000000
--- a/app/assets/images/emoji/roller_coaster.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rolling_eyes.png b/app/assets/images/emoji/rolling_eyes.png
deleted file mode 100644
index 2f77b9fc3b9..00000000000
--- a/app/assets/images/emoji/rolling_eyes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rooster.png b/app/assets/images/emoji/rooster.png
deleted file mode 100644
index bbf2bbff97a..00000000000
--- a/app/assets/images/emoji/rooster.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rose.png b/app/assets/images/emoji/rose.png
deleted file mode 100644
index 52c286d31ce..00000000000
--- a/app/assets/images/emoji/rose.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rosette.png b/app/assets/images/emoji/rosette.png
deleted file mode 100644
index 8030e494bcf..00000000000
--- a/app/assets/images/emoji/rosette.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rotating_light.png b/app/assets/images/emoji/rotating_light.png
deleted file mode 100644
index cad66b0afef..00000000000
--- a/app/assets/images/emoji/rotating_light.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/round_pushpin.png b/app/assets/images/emoji/round_pushpin.png
deleted file mode 100644
index 28b9d72866e..00000000000
--- a/app/assets/images/emoji/round_pushpin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rowboat.png b/app/assets/images/emoji/rowboat.png
deleted file mode 100644
index dd4dfc095d9..00000000000
--- a/app/assets/images/emoji/rowboat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone1.png b/app/assets/images/emoji/rowboat_tone1.png
deleted file mode 100644
index 5e5d18548cb..00000000000
--- a/app/assets/images/emoji/rowboat_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone2.png b/app/assets/images/emoji/rowboat_tone2.png
deleted file mode 100644
index 9b123ef8871..00000000000
--- a/app/assets/images/emoji/rowboat_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone3.png b/app/assets/images/emoji/rowboat_tone3.png
deleted file mode 100644
index 8ebd89a55f5..00000000000
--- a/app/assets/images/emoji/rowboat_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone4.png b/app/assets/images/emoji/rowboat_tone4.png
deleted file mode 100644
index 2b0d04f8725..00000000000
--- a/app/assets/images/emoji/rowboat_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone5.png b/app/assets/images/emoji/rowboat_tone5.png
deleted file mode 100644
index b346f2dfc84..00000000000
--- a/app/assets/images/emoji/rowboat_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rugby_football.png b/app/assets/images/emoji/rugby_football.png
deleted file mode 100644
index b1872273436..00000000000
--- a/app/assets/images/emoji/rugby_football.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/runner.png b/app/assets/images/emoji/runner.png
deleted file mode 100644
index e914915976a..00000000000
--- a/app/assets/images/emoji/runner.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone1.png b/app/assets/images/emoji/runner_tone1.png
deleted file mode 100644
index 9355239a52d..00000000000
--- a/app/assets/images/emoji/runner_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone2.png b/app/assets/images/emoji/runner_tone2.png
deleted file mode 100644
index 6112fd5c376..00000000000
--- a/app/assets/images/emoji/runner_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone3.png b/app/assets/images/emoji/runner_tone3.png
deleted file mode 100644
index 625ec708f48..00000000000
--- a/app/assets/images/emoji/runner_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone4.png b/app/assets/images/emoji/runner_tone4.png
deleted file mode 100644
index 242f1b56337..00000000000
--- a/app/assets/images/emoji/runner_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone5.png b/app/assets/images/emoji/runner_tone5.png
deleted file mode 100644
index 2976c6f019f..00000000000
--- a/app/assets/images/emoji/runner_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/running_shirt_with_sash.png b/app/assets/images/emoji/running_shirt_with_sash.png
deleted file mode 100644
index 6d83c06b803..00000000000
--- a/app/assets/images/emoji/running_shirt_with_sash.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sa.png b/app/assets/images/emoji/sa.png
deleted file mode 100644
index 900f9633247..00000000000
--- a/app/assets/images/emoji/sa.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sagittarius.png b/app/assets/images/emoji/sagittarius.png
deleted file mode 100644
index f8d94ff2923..00000000000
--- a/app/assets/images/emoji/sagittarius.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sailboat.png b/app/assets/images/emoji/sailboat.png
deleted file mode 100644
index 772ef11da5d..00000000000
--- a/app/assets/images/emoji/sailboat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sake.png b/app/assets/images/emoji/sake.png
deleted file mode 100644
index 2933f5672c4..00000000000
--- a/app/assets/images/emoji/sake.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/salad.png b/app/assets/images/emoji/salad.png
deleted file mode 100644
index c89f9341158..00000000000
--- a/app/assets/images/emoji/salad.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sandal.png b/app/assets/images/emoji/sandal.png
deleted file mode 100644
index 9d9f5122b7a..00000000000
--- a/app/assets/images/emoji/sandal.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/santa.png b/app/assets/images/emoji/santa.png
deleted file mode 100644
index bc83ab80d52..00000000000
--- a/app/assets/images/emoji/santa.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone1.png b/app/assets/images/emoji/santa_tone1.png
deleted file mode 100644
index 5233ffb7174..00000000000
--- a/app/assets/images/emoji/santa_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone2.png b/app/assets/images/emoji/santa_tone2.png
deleted file mode 100644
index 4e845438197..00000000000
--- a/app/assets/images/emoji/santa_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone3.png b/app/assets/images/emoji/santa_tone3.png
deleted file mode 100644
index 7fc4f33b60f..00000000000
--- a/app/assets/images/emoji/santa_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone4.png b/app/assets/images/emoji/santa_tone4.png
deleted file mode 100644
index d1d5a15132d..00000000000
--- a/app/assets/images/emoji/santa_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone5.png b/app/assets/images/emoji/santa_tone5.png
deleted file mode 100644
index 4d697a01f24..00000000000
--- a/app/assets/images/emoji/santa_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/satellite.png b/app/assets/images/emoji/satellite.png
deleted file mode 100644
index db0372795f4..00000000000
--- a/app/assets/images/emoji/satellite.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/satellite_orbital.png b/app/assets/images/emoji/satellite_orbital.png
deleted file mode 100644
index 4ba55d6e297..00000000000
--- a/app/assets/images/emoji/satellite_orbital.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/saxophone.png b/app/assets/images/emoji/saxophone.png
deleted file mode 100644
index a392faec291..00000000000
--- a/app/assets/images/emoji/saxophone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scales.png b/app/assets/images/emoji/scales.png
deleted file mode 100644
index 0757eda1684..00000000000
--- a/app/assets/images/emoji/scales.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/school.png b/app/assets/images/emoji/school.png
deleted file mode 100644
index 269759534f0..00000000000
--- a/app/assets/images/emoji/school.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/school_satchel.png b/app/assets/images/emoji/school_satchel.png
deleted file mode 100644
index 9997c86e7dc..00000000000
--- a/app/assets/images/emoji/school_satchel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scissors.png b/app/assets/images/emoji/scissors.png
deleted file mode 100644
index 270571c8cdd..00000000000
--- a/app/assets/images/emoji/scissors.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scooter.png b/app/assets/images/emoji/scooter.png
deleted file mode 100644
index 4ab7ef59cd2..00000000000
--- a/app/assets/images/emoji/scooter.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scorpion.png b/app/assets/images/emoji/scorpion.png
deleted file mode 100644
index 449a6b281c9..00000000000
--- a/app/assets/images/emoji/scorpion.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scorpius.png b/app/assets/images/emoji/scorpius.png
deleted file mode 100644
index c31a9920455..00000000000
--- a/app/assets/images/emoji/scorpius.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scream.png b/app/assets/images/emoji/scream.png
deleted file mode 100644
index c3bea9f2510..00000000000
--- a/app/assets/images/emoji/scream.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scream_cat.png b/app/assets/images/emoji/scream_cat.png
deleted file mode 100644
index 15803ad8e6e..00000000000
--- a/app/assets/images/emoji/scream_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scroll.png b/app/assets/images/emoji/scroll.png
deleted file mode 100644
index 50ee5dcd4b9..00000000000
--- a/app/assets/images/emoji/scroll.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/seat.png b/app/assets/images/emoji/seat.png
deleted file mode 100644
index a6d72d95adb..00000000000
--- a/app/assets/images/emoji/seat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/second_place.png b/app/assets/images/emoji/second_place.png
deleted file mode 100644
index 17b011268b6..00000000000
--- a/app/assets/images/emoji/second_place.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/secret.png b/app/assets/images/emoji/secret.png
deleted file mode 100644
index 5fd72608e60..00000000000
--- a/app/assets/images/emoji/secret.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/see_no_evil.png b/app/assets/images/emoji/see_no_evil.png
deleted file mode 100644
index 5187e474531..00000000000
--- a/app/assets/images/emoji/see_no_evil.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/seedling.png b/app/assets/images/emoji/seedling.png
deleted file mode 100644
index ae0948bcfd6..00000000000
--- a/app/assets/images/emoji/seedling.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/selfie.png b/app/assets/images/emoji/selfie.png
deleted file mode 100644
index 6a1ba75c7e3..00000000000
--- a/app/assets/images/emoji/selfie.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone1.png b/app/assets/images/emoji/selfie_tone1.png
deleted file mode 100644
index 290e075b56f..00000000000
--- a/app/assets/images/emoji/selfie_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone2.png b/app/assets/images/emoji/selfie_tone2.png
deleted file mode 100644
index fcd9595b643..00000000000
--- a/app/assets/images/emoji/selfie_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone3.png b/app/assets/images/emoji/selfie_tone3.png
deleted file mode 100644
index f3a22fdf435..00000000000
--- a/app/assets/images/emoji/selfie_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone4.png b/app/assets/images/emoji/selfie_tone4.png
deleted file mode 100644
index cdecf6d9f4e..00000000000
--- a/app/assets/images/emoji/selfie_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone5.png b/app/assets/images/emoji/selfie_tone5.png
deleted file mode 100644
index 86acbb6c202..00000000000
--- a/app/assets/images/emoji/selfie_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/seven.png b/app/assets/images/emoji/seven.png
deleted file mode 100644
index 9b3476ae7c7..00000000000
--- a/app/assets/images/emoji/seven.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shallow_pan_of_food.png b/app/assets/images/emoji/shallow_pan_of_food.png
deleted file mode 100644
index 663a1006acd..00000000000
--- a/app/assets/images/emoji/shallow_pan_of_food.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shamrock.png b/app/assets/images/emoji/shamrock.png
deleted file mode 100644
index f202aecfe6f..00000000000
--- a/app/assets/images/emoji/shamrock.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shark.png b/app/assets/images/emoji/shark.png
deleted file mode 100644
index c75076d57d8..00000000000
--- a/app/assets/images/emoji/shark.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shaved_ice.png b/app/assets/images/emoji/shaved_ice.png
deleted file mode 100644
index 36dfb53ca93..00000000000
--- a/app/assets/images/emoji/shaved_ice.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sheep.png b/app/assets/images/emoji/sheep.png
deleted file mode 100644
index 102b8a52b28..00000000000
--- a/app/assets/images/emoji/sheep.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shell.png b/app/assets/images/emoji/shell.png
deleted file mode 100644
index 55721629f62..00000000000
--- a/app/assets/images/emoji/shell.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shield.png b/app/assets/images/emoji/shield.png
deleted file mode 100644
index 610bf033ce0..00000000000
--- a/app/assets/images/emoji/shield.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shinto_shrine.png b/app/assets/images/emoji/shinto_shrine.png
deleted file mode 100644
index 5a344975bf3..00000000000
--- a/app/assets/images/emoji/shinto_shrine.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ship.png b/app/assets/images/emoji/ship.png
deleted file mode 100644
index 62d54f7d6c9..00000000000
--- a/app/assets/images/emoji/ship.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shirt.png b/app/assets/images/emoji/shirt.png
deleted file mode 100644
index af08dec8b59..00000000000
--- a/app/assets/images/emoji/shirt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shopping_bags.png b/app/assets/images/emoji/shopping_bags.png
deleted file mode 100644
index 99f2a2b13ac..00000000000
--- a/app/assets/images/emoji/shopping_bags.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shopping_cart.png b/app/assets/images/emoji/shopping_cart.png
deleted file mode 100644
index 1086fe6e456..00000000000
--- a/app/assets/images/emoji/shopping_cart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shower.png b/app/assets/images/emoji/shower.png
deleted file mode 100644
index 156776a2e52..00000000000
--- a/app/assets/images/emoji/shower.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrimp.png b/app/assets/images/emoji/shrimp.png
deleted file mode 100644
index 49eff28a71e..00000000000
--- a/app/assets/images/emoji/shrimp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrug.png b/app/assets/images/emoji/shrug.png
deleted file mode 100644
index 76e63bfac77..00000000000
--- a/app/assets/images/emoji/shrug.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone1.png b/app/assets/images/emoji/shrug_tone1.png
deleted file mode 100644
index 1c895e64468..00000000000
--- a/app/assets/images/emoji/shrug_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone2.png b/app/assets/images/emoji/shrug_tone2.png
deleted file mode 100644
index 4e3ca8f8bac..00000000000
--- a/app/assets/images/emoji/shrug_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone3.png b/app/assets/images/emoji/shrug_tone3.png
deleted file mode 100644
index d1b16a19bb5..00000000000
--- a/app/assets/images/emoji/shrug_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone4.png b/app/assets/images/emoji/shrug_tone4.png
deleted file mode 100644
index 5fbef3f2255..00000000000
--- a/app/assets/images/emoji/shrug_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone5.png b/app/assets/images/emoji/shrug_tone5.png
deleted file mode 100644
index 4af2e28bc5c..00000000000
--- a/app/assets/images/emoji/shrug_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/signal_strength.png b/app/assets/images/emoji/signal_strength.png
deleted file mode 100644
index ee2b5a4b519..00000000000
--- a/app/assets/images/emoji/signal_strength.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/six.png b/app/assets/images/emoji/six.png
deleted file mode 100644
index 371b3acef2c..00000000000
--- a/app/assets/images/emoji/six.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/six_pointed_star.png b/app/assets/images/emoji/six_pointed_star.png
deleted file mode 100644
index 2eb1707458b..00000000000
--- a/app/assets/images/emoji/six_pointed_star.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ski.png b/app/assets/images/emoji/ski.png
deleted file mode 100644
index 4a2d2c12306..00000000000
--- a/app/assets/images/emoji/ski.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/skier.png b/app/assets/images/emoji/skier.png
deleted file mode 100644
index 2eb3bdce2af..00000000000
--- a/app/assets/images/emoji/skier.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/skull.png b/app/assets/images/emoji/skull.png
deleted file mode 100644
index 26abb17296a..00000000000
--- a/app/assets/images/emoji/skull.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/skull_crossbones.png b/app/assets/images/emoji/skull_crossbones.png
deleted file mode 100644
index b459df9227a..00000000000
--- a/app/assets/images/emoji/skull_crossbones.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sleeping.png b/app/assets/images/emoji/sleeping.png
deleted file mode 100644
index 9ecf600d6d8..00000000000
--- a/app/assets/images/emoji/sleeping.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sleeping_accommodation.png b/app/assets/images/emoji/sleeping_accommodation.png
deleted file mode 100644
index c739e7fb69b..00000000000
--- a/app/assets/images/emoji/sleeping_accommodation.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sleepy.png b/app/assets/images/emoji/sleepy.png
deleted file mode 100644
index 836b4107717..00000000000
--- a/app/assets/images/emoji/sleepy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/slight_frown.png b/app/assets/images/emoji/slight_frown.png
deleted file mode 100644
index b2f1d983d36..00000000000
--- a/app/assets/images/emoji/slight_frown.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/slight_smile.png b/app/assets/images/emoji/slight_smile.png
deleted file mode 100644
index ddd7d65dd3d..00000000000
--- a/app/assets/images/emoji/slight_smile.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/slot_machine.png b/app/assets/images/emoji/slot_machine.png
deleted file mode 100644
index ee71b6c268c..00000000000
--- a/app/assets/images/emoji/slot_machine.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/small_blue_diamond.png b/app/assets/images/emoji/small_blue_diamond.png
deleted file mode 100644
index b86b5bc4db3..00000000000
--- a/app/assets/images/emoji/small_blue_diamond.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/small_orange_diamond.png b/app/assets/images/emoji/small_orange_diamond.png
deleted file mode 100644
index e1c6ed9b2f8..00000000000
--- a/app/assets/images/emoji/small_orange_diamond.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/small_red_triangle.png b/app/assets/images/emoji/small_red_triangle.png
deleted file mode 100644
index 785887c195a..00000000000
--- a/app/assets/images/emoji/small_red_triangle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/small_red_triangle_down.png b/app/assets/images/emoji/small_red_triangle_down.png
deleted file mode 100644
index a83beff1914..00000000000
--- a/app/assets/images/emoji/small_red_triangle_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smile.png b/app/assets/images/emoji/smile.png
deleted file mode 100644
index aa47ffe978c..00000000000
--- a/app/assets/images/emoji/smile.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smile_cat.png b/app/assets/images/emoji/smile_cat.png
deleted file mode 100644
index 6f25f11dd3a..00000000000
--- a/app/assets/images/emoji/smile_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smiley.png b/app/assets/images/emoji/smiley.png
deleted file mode 100644
index 30957a65968..00000000000
--- a/app/assets/images/emoji/smiley.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smiley_cat.png b/app/assets/images/emoji/smiley_cat.png
deleted file mode 100644
index 163b57a3427..00000000000
--- a/app/assets/images/emoji/smiley_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smiling_imp.png b/app/assets/images/emoji/smiling_imp.png
deleted file mode 100644
index cc2c5f1ec72..00000000000
--- a/app/assets/images/emoji/smiling_imp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smirk.png b/app/assets/images/emoji/smirk.png
deleted file mode 100644
index 87852109988..00000000000
--- a/app/assets/images/emoji/smirk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smirk_cat.png b/app/assets/images/emoji/smirk_cat.png
deleted file mode 100644
index 9ac5954c199..00000000000
--- a/app/assets/images/emoji/smirk_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smoking.png b/app/assets/images/emoji/smoking.png
deleted file mode 100644
index 910f648c8f9..00000000000
--- a/app/assets/images/emoji/smoking.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/snail.png b/app/assets/images/emoji/snail.png
deleted file mode 100644
index f4ea071e2d3..00000000000
--- a/app/assets/images/emoji/snail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/snake.png b/app/assets/images/emoji/snake.png
deleted file mode 100644
index d0278a28d8c..00000000000
--- a/app/assets/images/emoji/snake.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sneezing_face.png b/app/assets/images/emoji/sneezing_face.png
deleted file mode 100644
index ccf07d4b64d..00000000000
--- a/app/assets/images/emoji/sneezing_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/snowboarder.png b/app/assets/images/emoji/snowboarder.png
deleted file mode 100644
index 6361c0f2c9d..00000000000
--- a/app/assets/images/emoji/snowboarder.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/snowflake.png b/app/assets/images/emoji/snowflake.png
deleted file mode 100644
index db319a77ec6..00000000000
--- a/app/assets/images/emoji/snowflake.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/snowman.png b/app/assets/images/emoji/snowman.png
deleted file mode 100644
index 20c177c2aff..00000000000
--- a/app/assets/images/emoji/snowman.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/snowman2.png b/app/assets/images/emoji/snowman2.png
deleted file mode 100644
index 896f28502af..00000000000
--- a/app/assets/images/emoji/snowman2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sob.png b/app/assets/images/emoji/sob.png
deleted file mode 100644
index 52e3517a1ee..00000000000
--- a/app/assets/images/emoji/sob.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/soccer.png b/app/assets/images/emoji/soccer.png
deleted file mode 100644
index 28cfa218d6d..00000000000
--- a/app/assets/images/emoji/soccer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/soon.png b/app/assets/images/emoji/soon.png
deleted file mode 100644
index 8cdfd86690d..00000000000
--- a/app/assets/images/emoji/soon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sos.png b/app/assets/images/emoji/sos.png
deleted file mode 100644
index d7d8c9953e4..00000000000
--- a/app/assets/images/emoji/sos.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sound.png b/app/assets/images/emoji/sound.png
deleted file mode 100644
index e75ddca53ba..00000000000
--- a/app/assets/images/emoji/sound.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/space_invader.png b/app/assets/images/emoji/space_invader.png
deleted file mode 100644
index 2e73f5f32e5..00000000000
--- a/app/assets/images/emoji/space_invader.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spades.png b/app/assets/images/emoji/spades.png
deleted file mode 100644
index f822f184cb0..00000000000
--- a/app/assets/images/emoji/spades.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spaghetti.png b/app/assets/images/emoji/spaghetti.png
deleted file mode 100644
index 89c24a321f1..00000000000
--- a/app/assets/images/emoji/spaghetti.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sparkle.png b/app/assets/images/emoji/sparkle.png
deleted file mode 100644
index 6aa7b6ec9cf..00000000000
--- a/app/assets/images/emoji/sparkle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sparkler.png b/app/assets/images/emoji/sparkler.png
deleted file mode 100644
index 30339cd6e09..00000000000
--- a/app/assets/images/emoji/sparkler.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sparkles.png b/app/assets/images/emoji/sparkles.png
deleted file mode 100644
index 169bc10b023..00000000000
--- a/app/assets/images/emoji/sparkles.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sparkling_heart.png b/app/assets/images/emoji/sparkling_heart.png
deleted file mode 100644
index 6709269454e..00000000000
--- a/app/assets/images/emoji/sparkling_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/speak_no_evil.png b/app/assets/images/emoji/speak_no_evil.png
deleted file mode 100644
index 9d9e07c974b..00000000000
--- a/app/assets/images/emoji/speak_no_evil.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/speaker.png b/app/assets/images/emoji/speaker.png
deleted file mode 100644
index 7bcffb8fc43..00000000000
--- a/app/assets/images/emoji/speaker.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/speaking_head.png b/app/assets/images/emoji/speaking_head.png
deleted file mode 100644
index 2df93aaae09..00000000000
--- a/app/assets/images/emoji/speaking_head.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/speech_balloon.png b/app/assets/images/emoji/speech_balloon.png
deleted file mode 100644
index a34ef741733..00000000000
--- a/app/assets/images/emoji/speech_balloon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/speech_left.png b/app/assets/images/emoji/speech_left.png
deleted file mode 100644
index 00c05959bcd..00000000000
--- a/app/assets/images/emoji/speech_left.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/speedboat.png b/app/assets/images/emoji/speedboat.png
deleted file mode 100644
index 74059d12de1..00000000000
--- a/app/assets/images/emoji/speedboat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spider.png b/app/assets/images/emoji/spider.png
deleted file mode 100644
index 3849fa90b94..00000000000
--- a/app/assets/images/emoji/spider.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spider_web.png b/app/assets/images/emoji/spider_web.png
deleted file mode 100644
index ba448ee7fba..00000000000
--- a/app/assets/images/emoji/spider_web.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spoon.png b/app/assets/images/emoji/spoon.png
deleted file mode 100644
index 3c4da766aee..00000000000
--- a/app/assets/images/emoji/spoon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spy.png b/app/assets/images/emoji/spy.png
deleted file mode 100644
index a729e9584d6..00000000000
--- a/app/assets/images/emoji/spy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone1.png b/app/assets/images/emoji/spy_tone1.png
deleted file mode 100644
index 2d1c022caee..00000000000
--- a/app/assets/images/emoji/spy_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone2.png b/app/assets/images/emoji/spy_tone2.png
deleted file mode 100644
index 548b9c26f5d..00000000000
--- a/app/assets/images/emoji/spy_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone3.png b/app/assets/images/emoji/spy_tone3.png
deleted file mode 100644
index b023f4b18e1..00000000000
--- a/app/assets/images/emoji/spy_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone4.png b/app/assets/images/emoji/spy_tone4.png
deleted file mode 100644
index d8300af492d..00000000000
--- a/app/assets/images/emoji/spy_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone5.png b/app/assets/images/emoji/spy_tone5.png
deleted file mode 100644
index ca1462595fa..00000000000
--- a/app/assets/images/emoji/spy_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/squid.png b/app/assets/images/emoji/squid.png
deleted file mode 100644
index d2af223f0cb..00000000000
--- a/app/assets/images/emoji/squid.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stadium.png b/app/assets/images/emoji/stadium.png
deleted file mode 100644
index 00cd6db5e29..00000000000
--- a/app/assets/images/emoji/stadium.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/star.png b/app/assets/images/emoji/star.png
deleted file mode 100644
index c930947076e..00000000000
--- a/app/assets/images/emoji/star.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/star2.png b/app/assets/images/emoji/star2.png
deleted file mode 100644
index 2f5cba592db..00000000000
--- a/app/assets/images/emoji/star2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/star_and_crescent.png b/app/assets/images/emoji/star_and_crescent.png
deleted file mode 100644
index e182636457d..00000000000
--- a/app/assets/images/emoji/star_and_crescent.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/star_of_david.png b/app/assets/images/emoji/star_of_david.png
deleted file mode 100644
index fc59d0dde24..00000000000
--- a/app/assets/images/emoji/star_of_david.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stars.png b/app/assets/images/emoji/stars.png
deleted file mode 100644
index aa45384d1c6..00000000000
--- a/app/assets/images/emoji/stars.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/station.png b/app/assets/images/emoji/station.png
deleted file mode 100644
index 5c26fee529c..00000000000
--- a/app/assets/images/emoji/station.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/statue_of_liberty.png b/app/assets/images/emoji/statue_of_liberty.png
deleted file mode 100644
index 05df8289b59..00000000000
--- a/app/assets/images/emoji/statue_of_liberty.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/steam_locomotive.png b/app/assets/images/emoji/steam_locomotive.png
deleted file mode 100644
index 9ac0d999c4c..00000000000
--- a/app/assets/images/emoji/steam_locomotive.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stew.png b/app/assets/images/emoji/stew.png
deleted file mode 100644
index 6b3f010c17a..00000000000
--- a/app/assets/images/emoji/stew.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stop_button.png b/app/assets/images/emoji/stop_button.png
deleted file mode 100644
index cfa99988ac2..00000000000
--- a/app/assets/images/emoji/stop_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stopwatch.png b/app/assets/images/emoji/stopwatch.png
deleted file mode 100644
index 8fae1c9a898..00000000000
--- a/app/assets/images/emoji/stopwatch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/straight_ruler.png b/app/assets/images/emoji/straight_ruler.png
deleted file mode 100644
index 1017b7433a1..00000000000
--- a/app/assets/images/emoji/straight_ruler.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/strawberry.png b/app/assets/images/emoji/strawberry.png
deleted file mode 100644
index 7bb86f0b29c..00000000000
--- a/app/assets/images/emoji/strawberry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stuck_out_tongue.png b/app/assets/images/emoji/stuck_out_tongue.png
deleted file mode 100644
index 25757341f96..00000000000
--- a/app/assets/images/emoji/stuck_out_tongue.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png b/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png
deleted file mode 100644
index 5c0401e9b1d..00000000000
--- a/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stuck_out_tongue_winking_eye.png b/app/assets/images/emoji/stuck_out_tongue_winking_eye.png
deleted file mode 100644
index 4817eaa3dc6..00000000000
--- a/app/assets/images/emoji/stuck_out_tongue_winking_eye.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stuffed_flatbread.png b/app/assets/images/emoji/stuffed_flatbread.png
deleted file mode 100644
index a2e10df40a5..00000000000
--- a/app/assets/images/emoji/stuffed_flatbread.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sun_with_face.png b/app/assets/images/emoji/sun_with_face.png
deleted file mode 100644
index 14a4ea971db..00000000000
--- a/app/assets/images/emoji/sun_with_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sunflower.png b/app/assets/images/emoji/sunflower.png
deleted file mode 100644
index 08cc07761ea..00000000000
--- a/app/assets/images/emoji/sunflower.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sunglasses.png b/app/assets/images/emoji/sunglasses.png
deleted file mode 100644
index 20011735110..00000000000
--- a/app/assets/images/emoji/sunglasses.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sunny.png b/app/assets/images/emoji/sunny.png
deleted file mode 100644
index fd521ae31a7..00000000000
--- a/app/assets/images/emoji/sunny.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sunrise.png b/app/assets/images/emoji/sunrise.png
deleted file mode 100644
index 4ad36003c20..00000000000
--- a/app/assets/images/emoji/sunrise.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sunrise_over_mountains.png b/app/assets/images/emoji/sunrise_over_mountains.png
deleted file mode 100644
index 2b99307344d..00000000000
--- a/app/assets/images/emoji/sunrise_over_mountains.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/surfer.png b/app/assets/images/emoji/surfer.png
deleted file mode 100644
index 3ab017adf4b..00000000000
--- a/app/assets/images/emoji/surfer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone1.png b/app/assets/images/emoji/surfer_tone1.png
deleted file mode 100644
index b5faaa524cc..00000000000
--- a/app/assets/images/emoji/surfer_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone2.png b/app/assets/images/emoji/surfer_tone2.png
deleted file mode 100644
index 6d92e412ff1..00000000000
--- a/app/assets/images/emoji/surfer_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone3.png b/app/assets/images/emoji/surfer_tone3.png
deleted file mode 100644
index f05ef59496e..00000000000
--- a/app/assets/images/emoji/surfer_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone4.png b/app/assets/images/emoji/surfer_tone4.png
deleted file mode 100644
index 35e143d19dc..00000000000
--- a/app/assets/images/emoji/surfer_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone5.png b/app/assets/images/emoji/surfer_tone5.png
deleted file mode 100644
index 38917658eac..00000000000
--- a/app/assets/images/emoji/surfer_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sushi.png b/app/assets/images/emoji/sushi.png
deleted file mode 100644
index f171fd2f7a1..00000000000
--- a/app/assets/images/emoji/sushi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/suspension_railway.png b/app/assets/images/emoji/suspension_railway.png
deleted file mode 100644
index a59d5f48c24..00000000000
--- a/app/assets/images/emoji/suspension_railway.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sweat.png b/app/assets/images/emoji/sweat.png
deleted file mode 100644
index f0dae7b7893..00000000000
--- a/app/assets/images/emoji/sweat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sweat_drops.png b/app/assets/images/emoji/sweat_drops.png
deleted file mode 100644
index 4106117ebc8..00000000000
--- a/app/assets/images/emoji/sweat_drops.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sweat_smile.png b/app/assets/images/emoji/sweat_smile.png
deleted file mode 100644
index cb18d9c899b..00000000000
--- a/app/assets/images/emoji/sweat_smile.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sweet_potato.png b/app/assets/images/emoji/sweet_potato.png
deleted file mode 100644
index 92a425f2e20..00000000000
--- a/app/assets/images/emoji/sweet_potato.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/swimmer.png b/app/assets/images/emoji/swimmer.png
deleted file mode 100644
index 55b4d72f9a7..00000000000
--- a/app/assets/images/emoji/swimmer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone1.png b/app/assets/images/emoji/swimmer_tone1.png
deleted file mode 100644
index 38441c9ca9a..00000000000
--- a/app/assets/images/emoji/swimmer_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone2.png b/app/assets/images/emoji/swimmer_tone2.png
deleted file mode 100644
index b0d43112444..00000000000
--- a/app/assets/images/emoji/swimmer_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone3.png b/app/assets/images/emoji/swimmer_tone3.png
deleted file mode 100644
index 211e77e2aa0..00000000000
--- a/app/assets/images/emoji/swimmer_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone4.png b/app/assets/images/emoji/swimmer_tone4.png
deleted file mode 100644
index f34c34db9d2..00000000000
--- a/app/assets/images/emoji/swimmer_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone5.png b/app/assets/images/emoji/swimmer_tone5.png
deleted file mode 100644
index 3e9231ff868..00000000000
--- a/app/assets/images/emoji/swimmer_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/symbols.png b/app/assets/images/emoji/symbols.png
deleted file mode 100644
index ac2fc1f358f..00000000000
--- a/app/assets/images/emoji/symbols.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/synagogue.png b/app/assets/images/emoji/synagogue.png
deleted file mode 100644
index ee347904c80..00000000000
--- a/app/assets/images/emoji/synagogue.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/syringe.png b/app/assets/images/emoji/syringe.png
deleted file mode 100644
index 71c1a9528d5..00000000000
--- a/app/assets/images/emoji/syringe.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/taco.png b/app/assets/images/emoji/taco.png
deleted file mode 100644
index 10e847a4619..00000000000
--- a/app/assets/images/emoji/taco.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tada.png b/app/assets/images/emoji/tada.png
deleted file mode 100644
index 0244d60f269..00000000000
--- a/app/assets/images/emoji/tada.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tanabata_tree.png b/app/assets/images/emoji/tanabata_tree.png
deleted file mode 100644
index 46fcb3a1aac..00000000000
--- a/app/assets/images/emoji/tanabata_tree.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tangerine.png b/app/assets/images/emoji/tangerine.png
deleted file mode 100644
index ab14e5378db..00000000000
--- a/app/assets/images/emoji/tangerine.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/taurus.png b/app/assets/images/emoji/taurus.png
deleted file mode 100644
index b2a370df42b..00000000000
--- a/app/assets/images/emoji/taurus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/taxi.png b/app/assets/images/emoji/taxi.png
deleted file mode 100644
index 55f4cc84797..00000000000
--- a/app/assets/images/emoji/taxi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tea.png b/app/assets/images/emoji/tea.png
deleted file mode 100644
index b53b98f0c45..00000000000
--- a/app/assets/images/emoji/tea.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/telephone.png b/app/assets/images/emoji/telephone.png
deleted file mode 100644
index a1e69f566bc..00000000000
--- a/app/assets/images/emoji/telephone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/telephone_receiver.png b/app/assets/images/emoji/telephone_receiver.png
deleted file mode 100644
index 69388316c35..00000000000
--- a/app/assets/images/emoji/telephone_receiver.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/telescope.png b/app/assets/images/emoji/telescope.png
deleted file mode 100644
index d63154614b5..00000000000
--- a/app/assets/images/emoji/telescope.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ten.png b/app/assets/images/emoji/ten.png
deleted file mode 100644
index 782d4004962..00000000000
--- a/app/assets/images/emoji/ten.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tennis.png b/app/assets/images/emoji/tennis.png
deleted file mode 100644
index 7e68ba8f301..00000000000
--- a/app/assets/images/emoji/tennis.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tent.png b/app/assets/images/emoji/tent.png
deleted file mode 100644
index 3fddcfc56eb..00000000000
--- a/app/assets/images/emoji/tent.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thermometer.png b/app/assets/images/emoji/thermometer.png
deleted file mode 100644
index b1147392426..00000000000
--- a/app/assets/images/emoji/thermometer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thermometer_face.png b/app/assets/images/emoji/thermometer_face.png
deleted file mode 100644
index 8fc57387563..00000000000
--- a/app/assets/images/emoji/thermometer_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thinking.png b/app/assets/images/emoji/thinking.png
deleted file mode 100644
index c18f6fd14ad..00000000000
--- a/app/assets/images/emoji/thinking.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/third_place.png b/app/assets/images/emoji/third_place.png
deleted file mode 100644
index 636e04a5950..00000000000
--- a/app/assets/images/emoji/third_place.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thought_balloon.png b/app/assets/images/emoji/thought_balloon.png
deleted file mode 100644
index 72fe8fa7022..00000000000
--- a/app/assets/images/emoji/thought_balloon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/three.png b/app/assets/images/emoji/three.png
deleted file mode 100644
index dbaa6183e72..00000000000
--- a/app/assets/images/emoji/three.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown.png b/app/assets/images/emoji/thumbsdown.png
deleted file mode 100644
index b63da2f20a8..00000000000
--- a/app/assets/images/emoji/thumbsdown.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone1.png b/app/assets/images/emoji/thumbsdown_tone1.png
deleted file mode 100644
index a1631af8e92..00000000000
--- a/app/assets/images/emoji/thumbsdown_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone2.png b/app/assets/images/emoji/thumbsdown_tone2.png
deleted file mode 100644
index 85fff82d595..00000000000
--- a/app/assets/images/emoji/thumbsdown_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone3.png b/app/assets/images/emoji/thumbsdown_tone3.png
deleted file mode 100644
index eeba3be80fd..00000000000
--- a/app/assets/images/emoji/thumbsdown_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone4.png b/app/assets/images/emoji/thumbsdown_tone4.png
deleted file mode 100644
index 1addafdaed0..00000000000
--- a/app/assets/images/emoji/thumbsdown_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone5.png b/app/assets/images/emoji/thumbsdown_tone5.png
deleted file mode 100644
index 37ec07b5721..00000000000
--- a/app/assets/images/emoji/thumbsdown_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup.png b/app/assets/images/emoji/thumbsup.png
deleted file mode 100644
index f9e6f13a34f..00000000000
--- a/app/assets/images/emoji/thumbsup.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone1.png b/app/assets/images/emoji/thumbsup_tone1.png
deleted file mode 100644
index 39684cd5cc7..00000000000
--- a/app/assets/images/emoji/thumbsup_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone2.png b/app/assets/images/emoji/thumbsup_tone2.png
deleted file mode 100644
index a9b59723573..00000000000
--- a/app/assets/images/emoji/thumbsup_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone3.png b/app/assets/images/emoji/thumbsup_tone3.png
deleted file mode 100644
index c5e29167015..00000000000
--- a/app/assets/images/emoji/thumbsup_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone4.png b/app/assets/images/emoji/thumbsup_tone4.png
deleted file mode 100644
index 5bf4857a884..00000000000
--- a/app/assets/images/emoji/thumbsup_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone5.png b/app/assets/images/emoji/thumbsup_tone5.png
deleted file mode 100644
index d829f787c61..00000000000
--- a/app/assets/images/emoji/thumbsup_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thunder_cloud_rain.png b/app/assets/images/emoji/thunder_cloud_rain.png
deleted file mode 100644
index 31a26a1b6ee..00000000000
--- a/app/assets/images/emoji/thunder_cloud_rain.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ticket.png b/app/assets/images/emoji/ticket.png
deleted file mode 100644
index 605936bb6b3..00000000000
--- a/app/assets/images/emoji/ticket.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tickets.png b/app/assets/images/emoji/tickets.png
deleted file mode 100644
index e510f4a7a50..00000000000
--- a/app/assets/images/emoji/tickets.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tiger.png b/app/assets/images/emoji/tiger.png
deleted file mode 100644
index a4d3ef086d4..00000000000
--- a/app/assets/images/emoji/tiger.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tiger2.png b/app/assets/images/emoji/tiger2.png
deleted file mode 100644
index 871a8b74d56..00000000000
--- a/app/assets/images/emoji/tiger2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/timer.png b/app/assets/images/emoji/timer.png
deleted file mode 100644
index 8a3be574c24..00000000000
--- a/app/assets/images/emoji/timer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tired_face.png b/app/assets/images/emoji/tired_face.png
deleted file mode 100644
index 4e01eff5b23..00000000000
--- a/app/assets/images/emoji/tired_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tm.png b/app/assets/images/emoji/tm.png
deleted file mode 100644
index 7a0c44a2c2b..00000000000
--- a/app/assets/images/emoji/tm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/toilet.png b/app/assets/images/emoji/toilet.png
deleted file mode 100644
index 1392f761835..00000000000
--- a/app/assets/images/emoji/toilet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tokyo_tower.png b/app/assets/images/emoji/tokyo_tower.png
deleted file mode 100644
index 37df7fc65b1..00000000000
--- a/app/assets/images/emoji/tokyo_tower.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tomato.png b/app/assets/images/emoji/tomato.png
deleted file mode 100644
index 497da8f6b22..00000000000
--- a/app/assets/images/emoji/tomato.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tone1.png b/app/assets/images/emoji/tone1.png
deleted file mode 100644
index c395f3d0d68..00000000000
--- a/app/assets/images/emoji/tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tone2.png b/app/assets/images/emoji/tone2.png
deleted file mode 100644
index 080847431c1..00000000000
--- a/app/assets/images/emoji/tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tone3.png b/app/assets/images/emoji/tone3.png
deleted file mode 100644
index 482dd403475..00000000000
--- a/app/assets/images/emoji/tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tone4.png b/app/assets/images/emoji/tone4.png
deleted file mode 100644
index 5cae8bb20b0..00000000000
--- a/app/assets/images/emoji/tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tone5.png b/app/assets/images/emoji/tone5.png
deleted file mode 100644
index 49d1a8c3a64..00000000000
--- a/app/assets/images/emoji/tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tongue.png b/app/assets/images/emoji/tongue.png
deleted file mode 100644
index 70ce9c1225f..00000000000
--- a/app/assets/images/emoji/tongue.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tools.png b/app/assets/images/emoji/tools.png
deleted file mode 100644
index 3c6049273a9..00000000000
--- a/app/assets/images/emoji/tools.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/top.png b/app/assets/images/emoji/top.png
deleted file mode 100644
index 49dea8c08b5..00000000000
--- a/app/assets/images/emoji/top.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tophat.png b/app/assets/images/emoji/tophat.png
deleted file mode 100644
index 131b657b109..00000000000
--- a/app/assets/images/emoji/tophat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/track_next.png b/app/assets/images/emoji/track_next.png
deleted file mode 100644
index f8880d33bab..00000000000
--- a/app/assets/images/emoji/track_next.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/track_previous.png b/app/assets/images/emoji/track_previous.png
deleted file mode 100644
index 1ffd0566cfc..00000000000
--- a/app/assets/images/emoji/track_previous.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/trackball.png b/app/assets/images/emoji/trackball.png
deleted file mode 100644
index 3bea84ad7ce..00000000000
--- a/app/assets/images/emoji/trackball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tractor.png b/app/assets/images/emoji/tractor.png
deleted file mode 100644
index c1bf8cae44f..00000000000
--- a/app/assets/images/emoji/tractor.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/traffic_light.png b/app/assets/images/emoji/traffic_light.png
deleted file mode 100644
index 6b312285b00..00000000000
--- a/app/assets/images/emoji/traffic_light.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/train.png b/app/assets/images/emoji/train.png
deleted file mode 100644
index 3c80321f7e8..00000000000
--- a/app/assets/images/emoji/train.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/train2.png b/app/assets/images/emoji/train2.png
deleted file mode 100644
index 367c7bc5d39..00000000000
--- a/app/assets/images/emoji/train2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tram.png b/app/assets/images/emoji/tram.png
deleted file mode 100644
index b6f0e69038f..00000000000
--- a/app/assets/images/emoji/tram.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/triangular_flag_on_post.png b/app/assets/images/emoji/triangular_flag_on_post.png
deleted file mode 100644
index c12d8b06886..00000000000
--- a/app/assets/images/emoji/triangular_flag_on_post.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/triangular_ruler.png b/app/assets/images/emoji/triangular_ruler.png
deleted file mode 100644
index 77dee9ee843..00000000000
--- a/app/assets/images/emoji/triangular_ruler.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/trident.png b/app/assets/images/emoji/trident.png
deleted file mode 100644
index 777a1dad121..00000000000
--- a/app/assets/images/emoji/trident.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/triumph.png b/app/assets/images/emoji/triumph.png
deleted file mode 100644
index 0be7a501969..00000000000
--- a/app/assets/images/emoji/triumph.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/trolleybus.png b/app/assets/images/emoji/trolleybus.png
deleted file mode 100644
index 139a9931b52..00000000000
--- a/app/assets/images/emoji/trolleybus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/trophy.png b/app/assets/images/emoji/trophy.png
deleted file mode 100644
index ac2895c1896..00000000000
--- a/app/assets/images/emoji/trophy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tropical_drink.png b/app/assets/images/emoji/tropical_drink.png
deleted file mode 100644
index cd714f81b36..00000000000
--- a/app/assets/images/emoji/tropical_drink.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tropical_fish.png b/app/assets/images/emoji/tropical_fish.png
deleted file mode 100644
index 252105235a6..00000000000
--- a/app/assets/images/emoji/tropical_fish.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/truck.png b/app/assets/images/emoji/truck.png
deleted file mode 100644
index 130de047f8b..00000000000
--- a/app/assets/images/emoji/truck.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/trumpet.png b/app/assets/images/emoji/trumpet.png
deleted file mode 100644
index 864ccbcd04a..00000000000
--- a/app/assets/images/emoji/trumpet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tulip.png b/app/assets/images/emoji/tulip.png
deleted file mode 100644
index f799d75c182..00000000000
--- a/app/assets/images/emoji/tulip.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tumbler_glass.png b/app/assets/images/emoji/tumbler_glass.png
deleted file mode 100644
index 7bf09229879..00000000000
--- a/app/assets/images/emoji/tumbler_glass.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/turkey.png b/app/assets/images/emoji/turkey.png
deleted file mode 100644
index 344af94c9ec..00000000000
--- a/app/assets/images/emoji/turkey.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/turtle.png b/app/assets/images/emoji/turtle.png
deleted file mode 100644
index c22f7519fe8..00000000000
--- a/app/assets/images/emoji/turtle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tv.png b/app/assets/images/emoji/tv.png
deleted file mode 100644
index 999f1fb5c6d..00000000000
--- a/app/assets/images/emoji/tv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/twisted_rightwards_arrows.png b/app/assets/images/emoji/twisted_rightwards_arrows.png
deleted file mode 100644
index 5904badde65..00000000000
--- a/app/assets/images/emoji/twisted_rightwards_arrows.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/two.png b/app/assets/images/emoji/two.png
deleted file mode 100644
index 927339c9bff..00000000000
--- a/app/assets/images/emoji/two.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/two_hearts.png b/app/assets/images/emoji/two_hearts.png
deleted file mode 100644
index 4d8c3386042..00000000000
--- a/app/assets/images/emoji/two_hearts.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/two_men_holding_hands.png b/app/assets/images/emoji/two_men_holding_hands.png
deleted file mode 100644
index a511fda822a..00000000000
--- a/app/assets/images/emoji/two_men_holding_hands.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/two_women_holding_hands.png b/app/assets/images/emoji/two_women_holding_hands.png
deleted file mode 100644
index b077cd3e40f..00000000000
--- a/app/assets/images/emoji/two_women_holding_hands.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u5272.png b/app/assets/images/emoji/u5272.png
deleted file mode 100644
index c4f837fe684..00000000000
--- a/app/assets/images/emoji/u5272.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u5408.png b/app/assets/images/emoji/u5408.png
deleted file mode 100644
index 8375ad9d9af..00000000000
--- a/app/assets/images/emoji/u5408.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u55b6.png b/app/assets/images/emoji/u55b6.png
deleted file mode 100644
index d21cb30eaf3..00000000000
--- a/app/assets/images/emoji/u55b6.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u6307.png b/app/assets/images/emoji/u6307.png
deleted file mode 100644
index 078e23e4ff3..00000000000
--- a/app/assets/images/emoji/u6307.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u6708.png b/app/assets/images/emoji/u6708.png
deleted file mode 100644
index c41bd36a26a..00000000000
--- a/app/assets/images/emoji/u6708.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u6709.png b/app/assets/images/emoji/u6709.png
deleted file mode 100644
index a4510de41c0..00000000000
--- a/app/assets/images/emoji/u6709.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u6e80.png b/app/assets/images/emoji/u6e80.png
deleted file mode 100644
index f9dea8b8833..00000000000
--- a/app/assets/images/emoji/u6e80.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u7121.png b/app/assets/images/emoji/u7121.png
deleted file mode 100644
index d3a19b420de..00000000000
--- a/app/assets/images/emoji/u7121.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u7533.png b/app/assets/images/emoji/u7533.png
deleted file mode 100644
index 6b7af0ee222..00000000000
--- a/app/assets/images/emoji/u7533.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u7981.png b/app/assets/images/emoji/u7981.png
deleted file mode 100644
index 4c704e03433..00000000000
--- a/app/assets/images/emoji/u7981.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u7a7a.png b/app/assets/images/emoji/u7a7a.png
deleted file mode 100644
index 47966c1ea93..00000000000
--- a/app/assets/images/emoji/u7a7a.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/umbrella.png b/app/assets/images/emoji/umbrella.png
deleted file mode 100644
index 5b35b7ff6a4..00000000000
--- a/app/assets/images/emoji/umbrella.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/umbrella2.png b/app/assets/images/emoji/umbrella2.png
deleted file mode 100644
index 97fe859e74f..00000000000
--- a/app/assets/images/emoji/umbrella2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/unamused.png b/app/assets/images/emoji/unamused.png
deleted file mode 100644
index 25e3677f2eb..00000000000
--- a/app/assets/images/emoji/unamused.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/underage.png b/app/assets/images/emoji/underage.png
deleted file mode 100644
index 6dfe6da51e2..00000000000
--- a/app/assets/images/emoji/underage.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/unicorn.png b/app/assets/images/emoji/unicorn.png
deleted file mode 100644
index 05a97969f7e..00000000000
--- a/app/assets/images/emoji/unicorn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/unlock.png b/app/assets/images/emoji/unlock.png
deleted file mode 100644
index 4a74a693911..00000000000
--- a/app/assets/images/emoji/unlock.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/up.png b/app/assets/images/emoji/up.png
deleted file mode 100644
index 0d42142ba04..00000000000
--- a/app/assets/images/emoji/up.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/upside_down.png b/app/assets/images/emoji/upside_down.png
deleted file mode 100644
index 128f31c9828..00000000000
--- a/app/assets/images/emoji/upside_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/urn.png b/app/assets/images/emoji/urn.png
deleted file mode 100644
index 6b5b3503438..00000000000
--- a/app/assets/images/emoji/urn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/v.png b/app/assets/images/emoji/v.png
deleted file mode 100644
index 70c5516ffee..00000000000
--- a/app/assets/images/emoji/v.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/v_tone1.png b/app/assets/images/emoji/v_tone1.png
deleted file mode 100644
index 6ac54a745f4..00000000000
--- a/app/assets/images/emoji/v_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/v_tone2.png b/app/assets/images/emoji/v_tone2.png
deleted file mode 100644
index 6dd9669866d..00000000000
--- a/app/assets/images/emoji/v_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/v_tone3.png b/app/assets/images/emoji/v_tone3.png
deleted file mode 100644
index a615e53f02f..00000000000
--- a/app/assets/images/emoji/v_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/v_tone4.png b/app/assets/images/emoji/v_tone4.png
deleted file mode 100644
index 33a34bd5a78..00000000000
--- a/app/assets/images/emoji/v_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/v_tone5.png b/app/assets/images/emoji/v_tone5.png
deleted file mode 100644
index 45ad14b6c9c..00000000000
--- a/app/assets/images/emoji/v_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vertical_traffic_light.png b/app/assets/images/emoji/vertical_traffic_light.png
deleted file mode 100644
index 8085973eecf..00000000000
--- a/app/assets/images/emoji/vertical_traffic_light.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vhs.png b/app/assets/images/emoji/vhs.png
deleted file mode 100644
index b9eb78ecd92..00000000000
--- a/app/assets/images/emoji/vhs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vibration_mode.png b/app/assets/images/emoji/vibration_mode.png
deleted file mode 100644
index cc46510e48e..00000000000
--- a/app/assets/images/emoji/vibration_mode.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/video_camera.png b/app/assets/images/emoji/video_camera.png
deleted file mode 100644
index 85b300d425c..00000000000
--- a/app/assets/images/emoji/video_camera.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/video_game.png b/app/assets/images/emoji/video_game.png
deleted file mode 100644
index 316a9106a55..00000000000
--- a/app/assets/images/emoji/video_game.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/violin.png b/app/assets/images/emoji/violin.png
deleted file mode 100644
index e1e76cce242..00000000000
--- a/app/assets/images/emoji/violin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/virgo.png b/app/assets/images/emoji/virgo.png
deleted file mode 100644
index a6b56c2cb5e..00000000000
--- a/app/assets/images/emoji/virgo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/volcano.png b/app/assets/images/emoji/volcano.png
deleted file mode 100644
index 931d569294c..00000000000
--- a/app/assets/images/emoji/volcano.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/volleyball.png b/app/assets/images/emoji/volleyball.png
deleted file mode 100644
index 7a0e49d4b07..00000000000
--- a/app/assets/images/emoji/volleyball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vs.png b/app/assets/images/emoji/vs.png
deleted file mode 100644
index e1180f4a464..00000000000
--- a/app/assets/images/emoji/vs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vulcan.png b/app/assets/images/emoji/vulcan.png
deleted file mode 100644
index 54728bcaf5c..00000000000
--- a/app/assets/images/emoji/vulcan.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone1.png b/app/assets/images/emoji/vulcan_tone1.png
deleted file mode 100644
index 8aff5d8fa16..00000000000
--- a/app/assets/images/emoji/vulcan_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone2.png b/app/assets/images/emoji/vulcan_tone2.png
deleted file mode 100644
index 82b7ad519b4..00000000000
--- a/app/assets/images/emoji/vulcan_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone3.png b/app/assets/images/emoji/vulcan_tone3.png
deleted file mode 100644
index d1400e1dd28..00000000000
--- a/app/assets/images/emoji/vulcan_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone4.png b/app/assets/images/emoji/vulcan_tone4.png
deleted file mode 100644
index 47e2b280148..00000000000
--- a/app/assets/images/emoji/vulcan_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone5.png b/app/assets/images/emoji/vulcan_tone5.png
deleted file mode 100644
index 60b5c6077be..00000000000
--- a/app/assets/images/emoji/vulcan_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/walking.png b/app/assets/images/emoji/walking.png
deleted file mode 100644
index 06dc169a3fd..00000000000
--- a/app/assets/images/emoji/walking.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone1.png b/app/assets/images/emoji/walking_tone1.png
deleted file mode 100644
index 4e391b45a0b..00000000000
--- a/app/assets/images/emoji/walking_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone2.png b/app/assets/images/emoji/walking_tone2.png
deleted file mode 100644
index 31f94a1bce1..00000000000
--- a/app/assets/images/emoji/walking_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone3.png b/app/assets/images/emoji/walking_tone3.png
deleted file mode 100644
index f7ed8e39c2e..00000000000
--- a/app/assets/images/emoji/walking_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone4.png b/app/assets/images/emoji/walking_tone4.png
deleted file mode 100644
index e58dc04c7b2..00000000000
--- a/app/assets/images/emoji/walking_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone5.png b/app/assets/images/emoji/walking_tone5.png
deleted file mode 100644
index ba4e1b58fcb..00000000000
--- a/app/assets/images/emoji/walking_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/waning_crescent_moon.png b/app/assets/images/emoji/waning_crescent_moon.png
deleted file mode 100644
index cf68706b871..00000000000
--- a/app/assets/images/emoji/waning_crescent_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/waning_gibbous_moon.png b/app/assets/images/emoji/waning_gibbous_moon.png
deleted file mode 100644
index 24e16266119..00000000000
--- a/app/assets/images/emoji/waning_gibbous_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/warning.png b/app/assets/images/emoji/warning.png
deleted file mode 100644
index 35691c2ed97..00000000000
--- a/app/assets/images/emoji/warning.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wastebasket.png b/app/assets/images/emoji/wastebasket.png
deleted file mode 100644
index 2b3c484b498..00000000000
--- a/app/assets/images/emoji/wastebasket.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/watch.png b/app/assets/images/emoji/watch.png
deleted file mode 100644
index 64819bc6e21..00000000000
--- a/app/assets/images/emoji/watch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_buffalo.png b/app/assets/images/emoji/water_buffalo.png
deleted file mode 100644
index 80446615caf..00000000000
--- a/app/assets/images/emoji/water_buffalo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_polo.png b/app/assets/images/emoji/water_polo.png
deleted file mode 100644
index cb44576780d..00000000000
--- a/app/assets/images/emoji/water_polo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone1.png b/app/assets/images/emoji/water_polo_tone1.png
deleted file mode 100644
index bed1a908d6a..00000000000
--- a/app/assets/images/emoji/water_polo_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone2.png b/app/assets/images/emoji/water_polo_tone2.png
deleted file mode 100644
index ec5a43b4d4a..00000000000
--- a/app/assets/images/emoji/water_polo_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone3.png b/app/assets/images/emoji/water_polo_tone3.png
deleted file mode 100644
index b081a4a5a96..00000000000
--- a/app/assets/images/emoji/water_polo_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone4.png b/app/assets/images/emoji/water_polo_tone4.png
deleted file mode 100644
index 82cfbc3b0c7..00000000000
--- a/app/assets/images/emoji/water_polo_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone5.png b/app/assets/images/emoji/water_polo_tone5.png
deleted file mode 100644
index bd3366eb06c..00000000000
--- a/app/assets/images/emoji/water_polo_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/watermelon.png b/app/assets/images/emoji/watermelon.png
deleted file mode 100644
index 0761488b4c9..00000000000
--- a/app/assets/images/emoji/watermelon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wave.png b/app/assets/images/emoji/wave.png
deleted file mode 100644
index e0cd79b45f5..00000000000
--- a/app/assets/images/emoji/wave.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone1.png b/app/assets/images/emoji/wave_tone1.png
deleted file mode 100644
index 6b2b34b106e..00000000000
--- a/app/assets/images/emoji/wave_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone2.png b/app/assets/images/emoji/wave_tone2.png
deleted file mode 100644
index b857119732e..00000000000
--- a/app/assets/images/emoji/wave_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone3.png b/app/assets/images/emoji/wave_tone3.png
deleted file mode 100644
index 6283b670f43..00000000000
--- a/app/assets/images/emoji/wave_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone4.png b/app/assets/images/emoji/wave_tone4.png
deleted file mode 100644
index fe6b2baa747..00000000000
--- a/app/assets/images/emoji/wave_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone5.png b/app/assets/images/emoji/wave_tone5.png
deleted file mode 100644
index 4bd168ebb78..00000000000
--- a/app/assets/images/emoji/wave_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wavy_dash.png b/app/assets/images/emoji/wavy_dash.png
deleted file mode 100644
index 001c8d6e47d..00000000000
--- a/app/assets/images/emoji/wavy_dash.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/waxing_crescent_moon.png b/app/assets/images/emoji/waxing_crescent_moon.png
deleted file mode 100644
index 687125173d9..00000000000
--- a/app/assets/images/emoji/waxing_crescent_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/waxing_gibbous_moon.png b/app/assets/images/emoji/waxing_gibbous_moon.png
deleted file mode 100644
index 3a808156318..00000000000
--- a/app/assets/images/emoji/waxing_gibbous_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wc.png b/app/assets/images/emoji/wc.png
deleted file mode 100644
index aa433e84ba6..00000000000
--- a/app/assets/images/emoji/wc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/weary.png b/app/assets/images/emoji/weary.png
deleted file mode 100644
index 98bfbd24a16..00000000000
--- a/app/assets/images/emoji/weary.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wedding.png b/app/assets/images/emoji/wedding.png
deleted file mode 100644
index d0d8aa0bfae..00000000000
--- a/app/assets/images/emoji/wedding.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/whale.png b/app/assets/images/emoji/whale.png
deleted file mode 100644
index 9f19b44257c..00000000000
--- a/app/assets/images/emoji/whale.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/whale2.png b/app/assets/images/emoji/whale2.png
deleted file mode 100644
index 0df9d3c73a4..00000000000
--- a/app/assets/images/emoji/whale2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wheel_of_dharma.png b/app/assets/images/emoji/wheel_of_dharma.png
deleted file mode 100644
index 3666db0016b..00000000000
--- a/app/assets/images/emoji/wheel_of_dharma.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wheelchair.png b/app/assets/images/emoji/wheelchair.png
deleted file mode 100644
index 4e5b2698eac..00000000000
--- a/app/assets/images/emoji/wheelchair.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_check_mark.png b/app/assets/images/emoji/white_check_mark.png
deleted file mode 100644
index e55f087e544..00000000000
--- a/app/assets/images/emoji/white_check_mark.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_circle.png b/app/assets/images/emoji/white_circle.png
deleted file mode 100644
index c19e15684dd..00000000000
--- a/app/assets/images/emoji/white_circle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_flower.png b/app/assets/images/emoji/white_flower.png
deleted file mode 100644
index d6af8b60077..00000000000
--- a/app/assets/images/emoji/white_flower.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_large_square.png b/app/assets/images/emoji/white_large_square.png
deleted file mode 100644
index 6f06c1c79de..00000000000
--- a/app/assets/images/emoji/white_large_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_medium_small_square.png b/app/assets/images/emoji/white_medium_small_square.png
deleted file mode 100644
index ae874126750..00000000000
--- a/app/assets/images/emoji/white_medium_small_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_medium_square.png b/app/assets/images/emoji/white_medium_square.png
deleted file mode 100644
index 8daacf57059..00000000000
--- a/app/assets/images/emoji/white_medium_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_small_square.png b/app/assets/images/emoji/white_small_square.png
deleted file mode 100644
index d7ebdb0c0ed..00000000000
--- a/app/assets/images/emoji/white_small_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_square_button.png b/app/assets/images/emoji/white_square_button.png
deleted file mode 100644
index 934b1cedfd2..00000000000
--- a/app/assets/images/emoji/white_square_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_sun_cloud.png b/app/assets/images/emoji/white_sun_cloud.png
deleted file mode 100644
index 0a4cc100269..00000000000
--- a/app/assets/images/emoji/white_sun_cloud.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_sun_rain_cloud.png b/app/assets/images/emoji/white_sun_rain_cloud.png
deleted file mode 100644
index 491f9ca4839..00000000000
--- a/app/assets/images/emoji/white_sun_rain_cloud.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_sun_small_cloud.png b/app/assets/images/emoji/white_sun_small_cloud.png
deleted file mode 100644
index cead0bfa521..00000000000
--- a/app/assets/images/emoji/white_sun_small_cloud.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wilted_rose.png b/app/assets/images/emoji/wilted_rose.png
deleted file mode 100644
index 62412b143ae..00000000000
--- a/app/assets/images/emoji/wilted_rose.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wind_blowing_face.png b/app/assets/images/emoji/wind_blowing_face.png
deleted file mode 100644
index df81b652eb6..00000000000
--- a/app/assets/images/emoji/wind_blowing_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wind_chime.png b/app/assets/images/emoji/wind_chime.png
deleted file mode 100644
index 3c9ef3a95f6..00000000000
--- a/app/assets/images/emoji/wind_chime.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wine_glass.png b/app/assets/images/emoji/wine_glass.png
deleted file mode 100644
index 3cc98689192..00000000000
--- a/app/assets/images/emoji/wine_glass.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wink.png b/app/assets/images/emoji/wink.png
deleted file mode 100644
index 7ea7810a37d..00000000000
--- a/app/assets/images/emoji/wink.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wolf.png b/app/assets/images/emoji/wolf.png
deleted file mode 100644
index ba7220f2de9..00000000000
--- a/app/assets/images/emoji/wolf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/woman.png b/app/assets/images/emoji/woman.png
deleted file mode 100644
index ece440e7a61..00000000000
--- a/app/assets/images/emoji/woman.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone1.png b/app/assets/images/emoji/woman_tone1.png
deleted file mode 100644
index ff089b8889b..00000000000
--- a/app/assets/images/emoji/woman_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone2.png b/app/assets/images/emoji/woman_tone2.png
deleted file mode 100644
index 0719c378016..00000000000
--- a/app/assets/images/emoji/woman_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone3.png b/app/assets/images/emoji/woman_tone3.png
deleted file mode 100644
index 5672e2fd52d..00000000000
--- a/app/assets/images/emoji/woman_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone4.png b/app/assets/images/emoji/woman_tone4.png
deleted file mode 100644
index 5754aab558b..00000000000
--- a/app/assets/images/emoji/woman_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone5.png b/app/assets/images/emoji/woman_tone5.png
deleted file mode 100644
index fc252af3a39..00000000000
--- a/app/assets/images/emoji/woman_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/womans_clothes.png b/app/assets/images/emoji/womans_clothes.png
deleted file mode 100644
index 01410dc8107..00000000000
--- a/app/assets/images/emoji/womans_clothes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/womans_hat.png b/app/assets/images/emoji/womans_hat.png
deleted file mode 100644
index b837b6a2e47..00000000000
--- a/app/assets/images/emoji/womans_hat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/womens.png b/app/assets/images/emoji/womens.png
deleted file mode 100644
index d4ecc22e7b3..00000000000
--- a/app/assets/images/emoji/womens.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/worried.png b/app/assets/images/emoji/worried.png
deleted file mode 100644
index 7074afcf5b7..00000000000
--- a/app/assets/images/emoji/worried.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrench.png b/app/assets/images/emoji/wrench.png
deleted file mode 100644
index c16b7439697..00000000000
--- a/app/assets/images/emoji/wrench.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers.png b/app/assets/images/emoji/wrestlers.png
deleted file mode 100644
index 71e67cfad85..00000000000
--- a/app/assets/images/emoji/wrestlers.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone1.png b/app/assets/images/emoji/wrestlers_tone1.png
deleted file mode 100644
index 379070fd03b..00000000000
--- a/app/assets/images/emoji/wrestlers_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone2.png b/app/assets/images/emoji/wrestlers_tone2.png
deleted file mode 100644
index 6863ea9209d..00000000000
--- a/app/assets/images/emoji/wrestlers_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone3.png b/app/assets/images/emoji/wrestlers_tone3.png
deleted file mode 100644
index b7e62910127..00000000000
--- a/app/assets/images/emoji/wrestlers_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone4.png b/app/assets/images/emoji/wrestlers_tone4.png
deleted file mode 100644
index 750f9589233..00000000000
--- a/app/assets/images/emoji/wrestlers_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone5.png b/app/assets/images/emoji/wrestlers_tone5.png
deleted file mode 100644
index 36ab9bb3f42..00000000000
--- a/app/assets/images/emoji/wrestlers_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand.png b/app/assets/images/emoji/writing_hand.png
deleted file mode 100644
index 85639f8ac40..00000000000
--- a/app/assets/images/emoji/writing_hand.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone1.png b/app/assets/images/emoji/writing_hand_tone1.png
deleted file mode 100644
index 7923d8ebb17..00000000000
--- a/app/assets/images/emoji/writing_hand_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone2.png b/app/assets/images/emoji/writing_hand_tone2.png
deleted file mode 100644
index bcb304e15d2..00000000000
--- a/app/assets/images/emoji/writing_hand_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone3.png b/app/assets/images/emoji/writing_hand_tone3.png
deleted file mode 100644
index fd885fd2d90..00000000000
--- a/app/assets/images/emoji/writing_hand_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone4.png b/app/assets/images/emoji/writing_hand_tone4.png
deleted file mode 100644
index d065b8c64ab..00000000000
--- a/app/assets/images/emoji/writing_hand_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone5.png b/app/assets/images/emoji/writing_hand_tone5.png
deleted file mode 100644
index a44b3dd757c..00000000000
--- a/app/assets/images/emoji/writing_hand_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/x.png b/app/assets/images/emoji/x.png
deleted file mode 100644
index 9f9ed0f7ad2..00000000000
--- a/app/assets/images/emoji/x.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/yellow_heart.png b/app/assets/images/emoji/yellow_heart.png
deleted file mode 100644
index 7901a9d0103..00000000000
--- a/app/assets/images/emoji/yellow_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/yen.png b/app/assets/images/emoji/yen.png
deleted file mode 100644
index 63ee4799d66..00000000000
--- a/app/assets/images/emoji/yen.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/yin_yang.png b/app/assets/images/emoji/yin_yang.png
deleted file mode 100644
index f2900f6338f..00000000000
--- a/app/assets/images/emoji/yin_yang.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/yum.png b/app/assets/images/emoji/yum.png
deleted file mode 100644
index 2df15753ca1..00000000000
--- a/app/assets/images/emoji/yum.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/zap.png b/app/assets/images/emoji/zap.png
deleted file mode 100644
index 47e68e48e49..00000000000
--- a/app/assets/images/emoji/zap.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/zero.png b/app/assets/images/emoji/zero.png
deleted file mode 100644
index 13aca83e018..00000000000
--- a/app/assets/images/emoji/zero.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/zipper_mouth.png b/app/assets/images/emoji/zipper_mouth.png
deleted file mode 100644
index f8ced2502a7..00000000000
--- a/app/assets/images/emoji/zipper_mouth.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/zzz.png b/app/assets/images/emoji/zzz.png
deleted file mode 100644
index 9bc72b4469f..00000000000
--- a/app/assets/images/emoji/zzz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji@2x.png b/app/assets/images/emoji@2x.png
index 987279c13cc..8fe724329bf 100644
--- a/app/assets/images/emoji@2x.png
+++ b/app/assets/images/emoji@2x.png
Binary files differ
diff --git a/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql b/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql
index 60110437ecd..09278e1776a 100644
--- a/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql
+++ b/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql
@@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
-query getProjects(
+query accessTokensGetProjects(
$search: String = ""
$after: String = ""
$first: Int = null
diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js
index 2cd3a8f12ee..7f5f0403de6 100644
--- a/app/assets/javascripts/access_tokens/index.js
+++ b/app/assets/javascripts/access_tokens/index.js
@@ -49,7 +49,7 @@ export const initProjectsField = () => {
{ default: createDefaultClient },
]) => {
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/admin/deploy_keys/components/table.vue b/app/assets/javascripts/admin/deploy_keys/components/table.vue
new file mode 100644
index 00000000000..97a5a2f2f32
--- /dev/null
+++ b/app/assets/javascripts/admin/deploy_keys/components/table.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlTable, GlButton } from '@gitlab/ui';
+
+import { __ } from '~/locale';
+
+export default {
+ name: 'DeployKeysTable',
+ i18n: {
+ pageTitle: __('Public deploy keys'),
+ newDeployKeyButtonText: __('New deploy key'),
+ },
+ fields: [
+ {
+ key: 'title',
+ label: __('Title'),
+ },
+ {
+ key: 'fingerprint',
+ label: __('Fingerprint'),
+ },
+ {
+ key: 'projects',
+ label: __('Projects with write access'),
+ },
+ {
+ key: 'created',
+ label: __('Created'),
+ },
+ {
+ key: 'actions',
+ label: __('Actions'),
+ },
+ ],
+ components: {
+ GlTable,
+ GlButton,
+ },
+ inject: ['editPath', 'deletePath', 'createPath', 'emptyStateSvgPath'],
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-5">
+ <h4 class="gl-m-0">
+ {{ $options.i18n.pageTitle }}
+ </h4>
+ <gl-button variant="confirm" :href="createPath">{{
+ $options.i18n.newDeployKeyButtonText
+ }}</gl-button>
+ </div>
+ <gl-table :fields="$options.fields" data-testid="deploy-keys-list" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/deploy_keys/index.js b/app/assets/javascripts/admin/deploy_keys/index.js
new file mode 100644
index 00000000000..d86de4229de
--- /dev/null
+++ b/app/assets/javascripts/admin/deploy_keys/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import DeployKeysTable from './components/table.vue';
+
+export const initAdminDeployKeysTable = () => {
+ const el = document.getElementById('js-admin-deploy-keys-table');
+
+ if (!el) return false;
+
+ const { editPath, deletePath, createPath, emptyStateSvgPath } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ editPath,
+ deletePath,
+ createPath,
+ emptyStateSvgPath,
+ },
+ render(createElement) {
+ return createElement(DeployKeysTable);
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
index ed90343777d..e949498c55b 100644
--- a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
+++ b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
@@ -138,7 +138,7 @@ export default {
/>
</form>
<template #modal-footer>
- <gl-button @click="onCancel">{{ s__('Cancel') }}</gl-button>
+ <gl-button @click="onCancel">{{ __('Cancel') }}</gl-button>
<gl-button
:disabled="!canSubmit"
category="secondary"
diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js
index 852b253d25a..0c485d2a239 100644
--- a/app/assets/javascripts/admin/users/index.js
+++ b/app/assets/javascripts/admin/users/index.js
@@ -15,7 +15,7 @@ import {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
const initApp = (el, component, userPropKey, props = {}) => {
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index e59d7fc058a..79a6bac3ba7 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -17,7 +17,6 @@ import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue';
-import AlertsDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
import {
tdClass,
thClass,
@@ -26,7 +25,6 @@ import {
} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants';
import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ALERTS_STATUS_TABS, SEVERITY_LEVELS, trackAlertListViewsOptions } from '../constants';
import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
@@ -98,7 +96,6 @@ export default {
severityLabels: SEVERITY_LEVELS,
statusTabs: ALERTS_STATUS_TABS,
components: {
- AlertsDeprecationWarning,
GlAlert,
GlLoadingIcon,
GlTable,
@@ -115,7 +112,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagMixin()],
inject: ['projectPath', 'textQuery', 'assigneeUsernameQuery', 'populatingAlertsHelpUrl'],
apollo: {
alerts: {
@@ -277,8 +273,6 @@ export default {
</gl-sprintf>
</gl-alert>
- <alerts-deprecation-warning v-if="!glFeatures.managedAlertsDeprecation" />
-
<paginated-table-with-search-and-tabs
:show-error-msg="showErrorMsg"
:i18n="$options.i18n"
diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js
index 57d1f135606..b23f8a8eba4 100644
--- a/app/assets/javascripts/alert_management/list.js
+++ b/app/assets/javascripts/alert_management/list.js
@@ -23,7 +23,6 @@ export default () => {
assigneeUsernameQuery,
alertManagementEnabled,
userCanEnableAlertManagement,
- hasManagedPrometheus,
} = domEl.dataset;
const apolloProvider = new VueApollo({
@@ -39,7 +38,6 @@ export default () => {
return defaultDataIdFromObject(object);
},
},
- assumeImmutableResults: true,
},
),
});
@@ -66,7 +64,6 @@ export default () => {
alertManagementEnabled: parseBoolean(alertManagementEnabled),
trackAlertStatusUpdateOptions: PAGE_CONFIG.OPERATIONS.TRACK_ALERT_STATUS_UPDATE_OPTIONS,
userCanEnableAlertManagement: parseBoolean(userCanEnableAlertManagement),
- hasManagedPrometheus: parseBoolean(hasManagedPrometheus),
},
apolloProvider,
render(createElement) {
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index a5f7b84446f..6b5aac57f1c 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -16,6 +16,7 @@ import {
import * as Sentry from '@sentry/browser';
import { isEqual, isEmpty, omit } from 'lodash';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
import {
integrationTypes,
integrationSteps,
@@ -129,6 +130,7 @@ export default {
name: true,
apiUrl: true,
},
+ pricingLink: `${PROMO_URL}/pricing`,
};
},
computed: {
@@ -436,7 +438,7 @@ export default {
disabled="true"
class="gl-display-inline-block gl-my-4"
:message="$options.i18n.integrationFormSteps.selectType.enterprise"
- link="https://about.gitlab.com/pricing"
+ :link="pricingLink"
data-testid="multi-integrations-not-supported"
/>
</gl-form-group>
diff --git a/app/assets/javascripts/alerts_settings/graphql.js b/app/assets/javascripts/alerts_settings/graphql.js
index 15862f4034a..b64e2e3eefa 100644
--- a/app/assets/javascripts/alerts_settings/graphql.js
+++ b/app/assets/javascripts/alerts_settings/graphql.js
@@ -59,6 +59,5 @@ export default new VueApollo({
cacheConfig: {
fragmentMatcher,
},
- assumeImmutableResults: true,
}),
});
diff --git a/app/assets/javascripts/analytics/devops_report/constants.js b/app/assets/javascripts/analytics/devops_report/constants.js
deleted file mode 100644
index b395d7eb464..00000000000
--- a/app/assets/javascripts/analytics/devops_report/constants.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { __ } from '~/locale';
-
-export const INTRO_COOKIE_KEY = 'dev_ops_report_intro_callout_dismissed';
-
-export const INTRO_BANNER_TITLE = __('Introducing Your DevOps Report');
-
-export const INTRO_BANNER_BODY = __(
- 'Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations.',
-);
-
-export const INTRO_BANNER_ACTION_TEXT = __('Read more');
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue b/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
index 238081cc3c0..238081cc3c0 100644
--- a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
+++ b/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue b/app/assets/javascripts/analytics/devops_reports/components/devops_score_callout.vue
index e594b4e360a..e594b4e360a 100644
--- a/app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue
+++ b/app/assets/javascripts/analytics/devops_reports/components/devops_score_callout.vue
diff --git a/app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue b/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue
index 400326e41e1..400326e41e1 100644
--- a/app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue
+++ b/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue
diff --git a/app/assets/javascripts/analytics/devops_reports/constants.js b/app/assets/javascripts/analytics/devops_reports/constants.js
new file mode 100644
index 00000000000..6091fcb5724
--- /dev/null
+++ b/app/assets/javascripts/analytics/devops_reports/constants.js
@@ -0,0 +1,11 @@
+import { __ } from '~/locale';
+
+export const INTRO_COOKIE_KEY = 'dev_ops_report_intro_callout_dismissed';
+
+export const INTRO_BANNER_TITLE = __('Introducing Your DevOps Reports');
+
+export const INTRO_BANNER_BODY = __(
+ 'Your DevOps Reports give an overview of how you are using GitLab from a feature perspective. Use them to view how you compare with other organizations, and how your teams compare against each other.',
+);
+
+export const INTRO_BANNER_ACTION_TEXT = __('Read more');
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score.js b/app/assets/javascripts/analytics/devops_reports/devops_score.js
index 0bf98b65ed5..0bf98b65ed5 100644
--- a/app/assets/javascripts/analytics/devops_report/devops_score.js
+++ b/app/assets/javascripts/analytics/devops_reports/devops_score.js
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js b/app/assets/javascripts/analytics/devops_reports/devops_score_disabled_service_ping.js
index eb2992422a4..eb2992422a4 100644
--- a/app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js
+++ b/app/assets/javascripts/analytics/devops_reports/devops_score_disabled_service_ping.js
diff --git a/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql b/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
index 63e95d6804c..b870ed4dcbf 100644
--- a/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
+++ b/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
@@ -1,4 +1,4 @@
-query getGroupProjects(
+query analyticsGetGroupProjects(
$groupFullPath: ID!
$search: String!
$first: Int!
diff --git a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
index 1eb4832a2a3..63ec40d4ec6 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
@@ -3,7 +3,7 @@ import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import createFlash from '~/flash';
import { number } from '~/lib/utils/unit_format';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import usageTrendsCountQuery from '../graphql/queries/usage_trends_count.query.graphql';
const defaultPrecision = 0;
@@ -52,7 +52,7 @@ export default {
mergeRequests: s__('UsageTrends|Merge requests'),
pipelines: s__('UsageTrends|Pipelines'),
},
- loadCountsError: s__('Could not load usage counts. Please refresh the page to try again.'),
+ loadCountsError: __('Could not load usage counts. Please refresh the page to try again.'),
},
};
</script>
diff --git a/app/assets/javascripts/analytics/usage_trends/index.js b/app/assets/javascripts/analytics/usage_trends/index.js
index 3e85832edcf..d1880b09f15 100644
--- a/app/assets/javascripts/analytics/usage_trends/index.js
+++ b/app/assets/javascripts/analytics/usage_trends/index.js
@@ -6,7 +6,7 @@ import UsageTrendsApp from './components/app.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
export default () => {
diff --git a/app/assets/javascripts/api/namespaces_api.js b/app/assets/javascripts/api/namespaces_api.js
new file mode 100644
index 00000000000..166a95b749a
--- /dev/null
+++ b/app/assets/javascripts/api/namespaces_api.js
@@ -0,0 +1,13 @@
+import { buildApiUrl } from '~/api/api_utils';
+import axios from '~/lib/utils/axios_utils';
+
+const NAMESPACE_EXISTS_PATH = '/api/:version/namespaces/:id/exists';
+
+export function getGroupPathAvailability(groupPath, parentId, axiosOptions = {}) {
+ const url = buildApiUrl(NAMESPACE_EXISTS_PATH).replace(':id', encodeURIComponent(groupPath));
+
+ return axios.get(url, {
+ params: { parent_id: parentId, ...axiosOptions.params },
+ ...axiosOptions,
+ });
+}
diff --git a/app/assets/javascripts/artifacts_settings/index.js b/app/assets/javascripts/artifacts_settings/index.js
index 5c9f1c3129c..531b42bc185 100644
--- a/app/assets/javascripts/artifacts_settings/index.js
+++ b/app/assets/javascripts/artifacts_settings/index.js
@@ -6,7 +6,7 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
export default (containerId = 'js-artifacts-settings-app') => {
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
index 0b748f18cb2..484c6524d0e 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
@@ -1,26 +1,46 @@
<script>
-import { GlFormInput, GlFormGroup, GlButton, GlForm } from '@gitlab/ui';
+import { GlFormInput, GlFormGroup, GlButton, GlForm, GlModal } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __ } from '~/locale';
export const i18n = {
currentPassword: __('Current password'),
+ confirmTitle: __('Are you sure?'),
confirmWebAuthn: __(
- 'Are you sure? This will invalidate your registered applications and U2F / WebAuthn devices.',
+ 'This will invalidate your registered applications and U2F / WebAuthn devices.',
),
- confirm: __('Are you sure? This will invalidate your registered applications and U2F devices.'),
+ confirm: __('This will invalidate your registered applications and U2F devices.'),
disableTwoFactor: __('Disable two-factor authentication'),
+ disable: __('Disable'),
+ cancel: __('Cancel'),
regenerateRecoveryCodes: __('Regenerate recovery codes'),
+ currentPasswordInvalidFeedback: __('Please enter your current password.'),
};
export default {
name: 'ManageTwoFactorForm',
i18n,
+ modalId: 'manage-two-factor-auth-confirm-modal',
+ modalActions: {
+ primary: {
+ text: i18n.disable,
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ secondary: {
+ text: i18n.cancel,
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
components: {
GlForm,
GlFormInput,
GlFormGroup,
GlButton,
+ GlModal,
},
inject: [
'webauthnEnabled',
@@ -32,8 +52,11 @@ export default {
],
data() {
return {
- method: '',
- action: '#',
+ method: null,
+ action: null,
+ currentPassword: '',
+ currentPasswordState: null,
+ showConfirmModal: false,
};
},
computed: {
@@ -46,9 +69,34 @@ export default {
},
},
methods: {
- handleFormSubmit(event) {
- this.method = event.submitter.dataset.formMethod;
- this.action = event.submitter.dataset.formAction;
+ submitForm() {
+ this.$refs.form.$el.submit();
+ },
+ async handleSubmitButtonClick({ method, action, confirm = false }) {
+ this.method = method;
+ this.action = action;
+
+ if (this.isCurrentPasswordRequired && this.currentPassword === '') {
+ this.currentPasswordState = false;
+
+ return;
+ }
+
+ this.currentPasswordState = null;
+
+ if (confirm) {
+ this.showConfirmModal = true;
+
+ return;
+ }
+
+ // Wait for form action and method to be updated
+ await this.$nextTick();
+
+ this.submitForm();
+ },
+ handleModalPrimary() {
+ this.submitForm();
},
},
csrf,
@@ -57,10 +105,11 @@ export default {
<template>
<gl-form
- class="gl-display-inline-block"
+ ref="form"
+ class="gl-sm-display-inline-block"
method="post"
:action="action"
- @submit="handleFormSubmit($event)"
+ @submit.prevent
>
<input type="hidden" name="_method" data-testid="test-2fa-method-field" :value="method" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
@@ -69,35 +118,59 @@ export default {
v-if="isCurrentPasswordRequired"
:label="$options.i18n.currentPassword"
label-for="current-password"
+ :state="currentPasswordState"
+ :invalid-feedback="$options.i18n.currentPasswordInvalidFeedback"
>
<gl-form-input
id="current-password"
+ v-model="currentPassword"
type="password"
name="current_password"
- required
+ :state="currentPasswordState"
data-qa-selector="current_password_field"
/>
</gl-form-group>
- <gl-button
- type="submit"
- class="btn-danger gl-mr-3 gl-display-inline-block"
- data-testid="test-2fa-disable-button"
- variant="danger"
- :data-confirm="confirmText"
- :data-form-action="profileTwoFactorAuthPath"
- :data-form-method="profileTwoFactorAuthMethod"
- >
- {{ $options.i18n.disableTwoFactor }}
- </gl-button>
- <gl-button
- type="submit"
- class="gl-display-inline-block"
- data-testid="test-2fa-regenerate-codes-button"
- :data-form-action="codesProfileTwoFactorAuthPath"
- :data-form-method="codesProfileTwoFactorAuthMethod"
+ <div class="gl-display-flex gl-flex-wrap">
+ <gl-button
+ type="submit"
+ class="gl-sm-mr-3 gl-w-full gl-sm-w-auto"
+ data-testid="test-2fa-disable-button"
+ variant="danger"
+ @click.prevent="
+ handleSubmitButtonClick({
+ method: profileTwoFactorAuthMethod,
+ action: profileTwoFactorAuthPath,
+ confirm: true,
+ })
+ "
+ >
+ {{ $options.i18n.disableTwoFactor }}
+ </gl-button>
+ <gl-button
+ type="submit"
+ class="gl-mt-3 gl-sm-mt-0 gl-w-full gl-sm-w-auto"
+ data-testid="test-2fa-regenerate-codes-button"
+ @click.prevent="
+ handleSubmitButtonClick({
+ method: codesProfileTwoFactorAuthMethod,
+ action: codesProfileTwoFactorAuthPath,
+ })
+ "
+ >
+ {{ $options.i18n.regenerateRecoveryCodes }}
+ </gl-button>
+ </div>
+ <gl-modal
+ v-model="showConfirmModal"
+ :modal-id="$options.modalId"
+ size="sm"
+ :title="$options.i18n.confirmTitle"
+ :action-primary="$options.modalActions.primary"
+ :action-secondary="$options.modalActions.secondary"
+ @primary="handleModalPrimary"
>
- {{ $options.i18n.regenerateRecoveryCodes }}
- </gl-button>
+ {{ confirmText }}
+ </gl-modal>
</gl-form>
</template>
diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue
index 825807e833e..0303930de5d 100644
--- a/app/assets/javascripts/badges/components/badge_settings.vue
+++ b/app/assets/javascripts/badges/components/badge_settings.vue
@@ -2,7 +2,7 @@
import { GlSprintf, GlModal } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import createFlash from '~/flash';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import Badge from './badge.vue';
import BadgeForm from './badge_form.vue';
import BadgeList from './badge_list.vue';
@@ -25,13 +25,13 @@ export default {
...mapState(['badgeInModal', 'isEditing']),
primaryProps() {
return {
- text: s__('Delete badge'),
+ text: __('Delete badge'),
attributes: [{ category: 'primary' }, { variant: 'danger' }],
};
},
cancelProps() {
return {
- text: s__('Cancel'),
+ text: __('Cancel'),
};
},
},
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index f5e3bab6ff0..918519f386b 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import NoteableNote from '~/notes/components/noteable_note.vue';
import PublishButton from './publish_button.vue';
@@ -10,6 +10,9 @@ export default {
PublishButton,
GlButton,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
props: {
draft: {
type: Object,
@@ -72,6 +75,9 @@ export default {
}
},
},
+ safeHtmlConfig: {
+ ADD_TAGS: ['use', 'gl-emoji'],
+ },
};
</script>
<template>
@@ -104,8 +110,8 @@ export default {
<template v-if="!isEditingDraft">
<div
v-if="draftCommands"
+ v-safe-html:[$options.safeHtmlConfig]="draftCommands"
class="referenced-commands draft-note-commands"
- v-html="draftCommands /* eslint-disable-line vue/no-v-html */"
></div>
<p class="draft-note-actions d-flex">
diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
index 91b3b6a685c..e90c29e939f 100644
--- a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
@@ -15,14 +15,14 @@ export default {
...mapGetters('batchComments', ['draftsCount', 'sortedDrafts']),
},
methods: {
- ...mapActions('diffs', ['toggleActiveFileByHash']),
+ ...mapActions('diffs', ['setCurrentFileHash']),
...mapActions('batchComments', ['scrollToDraft']),
isLast(index) {
return index === this.sortedDrafts.length - 1;
},
async onClickDraft(draft) {
if (this.viewDiffsFileByFile && draft.file_hash) {
- await this.toggleActiveFileByHash(draft.file_hash);
+ await this.setCurrentFileHash(draft.file_hash);
}
await this.scrollToDraft(draft);
diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue
index 213e026c41f..e3e43ea3a0e 100644
--- a/app/assets/javascripts/blob/components/blob_content.vue
+++ b/app/assets/javascripts/blob/components/blob_content.vue
@@ -65,7 +65,7 @@ export default {
};
</script>
<template>
- <div class="blob-viewer" :data-type="activeViewer.type">
+ <div class="blob-viewer" :data-type="activeViewer.type" :data-loaded="!loading">
<gl-loading-icon v-if="loading" size="md" color="dark" class="my-4 mx-auto" />
<template v-else>
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 4742b4ae4b4..933ad448c77 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -3,12 +3,14 @@ import DefaultActions from './blob_header_default_actions.vue';
import BlobFilepath from './blob_header_filepath.vue';
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
import { SIMPLE_BLOB_VIEWER } from './constants';
+import TableOfContents from './table_contents.vue';
export default {
components: {
ViewerSwitcher,
DefaultActions,
BlobFilepath,
+ TableOfContents,
},
props: {
blob: {
@@ -70,11 +72,14 @@ export default {
</script>
<template>
<div class="js-file-title file-title-flex-parent">
- <blob-filepath :blob="blob">
- <template #filepath-prepend>
- <slot name="prepend"></slot>
- </template>
- </blob-filepath>
+ <div class="gl-display-flex">
+ <table-of-contents class="gl-pr-2" />
+ <blob-filepath :blob="blob">
+ <template #filepath-prepend>
+ <slot name="prepend"></slot>
+ </template>
+ </blob-filepath>
+ </div>
<div class="gl-display-none gl-sm-display-flex">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" />
diff --git a/app/assets/javascripts/blob/components/table_contents.vue b/app/assets/javascripts/blob/components/table_contents.vue
index 78ecb82f2cd..07da262ec9a 100644
--- a/app/assets/javascripts/blob/components/table_contents.vue
+++ b/app/assets/javascripts/blob/components/table_contents.vue
@@ -18,11 +18,12 @@ export default {
},
mounted() {
this.blobViewer = document.querySelector('.blob-viewer[data-type="rich"]');
+ const blobViewerAttr = (attr) => this.blobViewer.getAttribute(attr);
this.observer = new MutationObserver(() => {
- if (this.blobViewer.classList.contains('hidden')) {
+ if (this.blobViewer.classList.contains('hidden') || blobViewerAttr('data-type') !== 'rich') {
this.isHidden = true;
- } else if (this.blobViewer.getAttribute('data-loaded') === 'true') {
+ } else if (blobViewerAttr('data-loaded') === 'true') {
this.isHidden = false;
this.generateHeaders();
}
diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
index a3278f8bde2..e75aa523ed0 100644
--- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
+++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
@@ -1,7 +1,7 @@
<script>
import { GlModal, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
import Cookies from 'js-cookie';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
const trackingMixin = Tracking.mixin();
@@ -69,7 +69,7 @@ export default {
},
},
i18n: {
- modalTitle: s__("That's it, well done!"),
+ modalTitle: __("That's it, well done!"),
pipelinesButton: s__('MR widget|See your pipeline in action'),
mergeRequestButton: s__('MR widget|Back to the Merge request'),
bodyMessage: s__(
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index c10241d00d7..e6c91c7ac1f 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,4 +1,5 @@
import { sortBy, cloneDeep } from 'lodash';
+import { isGid } from '~/graphql_shared/utils';
import { ListType, MilestoneIDs } from './constants';
export function getMilestone() {
@@ -95,6 +96,9 @@ export function fullMilestoneId(id) {
}
export function fullLabelId(label) {
+ if (isGid(label.id)) {
+ return label.id;
+ }
if (label.project_id && label.project_id !== null) {
return `gid://gitlab/ProjectLabel/${label.id}`;
}
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 1e780f9ef84..563bed6a6b8 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -83,7 +83,7 @@ export default {
:data-item-path="item.referencePath"
data-testid="board_card"
class="board-card gl-p-5 gl-rounded-base"
- @mouseup="toggleIssue($event)"
+ @click="toggleIssue($event)"
>
<board-card-inner :list="list" :item="item" :update-filters="true" />
</li>
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 9bbb8a1a1b2..54668c9e88e 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -15,6 +15,7 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
@@ -53,6 +54,9 @@ export default {
allowLabelEdit: {
default: false,
},
+ labelsFilterBasePath: {
+ default: '',
+ },
},
inheritAttrs: false,
computed: {
@@ -63,7 +67,7 @@ export default {
'groupPathForActiveIssue',
'projectPathForActiveIssue',
]),
- ...mapState(['sidebarType', 'issuableType', 'isSettingLabels']),
+ ...mapState(['sidebarType', 'issuableType']),
isIssuableSidebar() {
return this.sidebarType === ISSUABLE;
},
@@ -84,7 +88,15 @@ export default {
});
},
attrWorkspacePath() {
- return this.isGroupBoard ? this.groupPathForActiveIssue : undefined;
+ return this.isGroupBoard ? this.groupPathForActiveIssue : this.projectPathForActiveIssue;
+ },
+ labelType() {
+ return this.isGroupBoard ? LabelType.group : LabelType.project;
+ },
+ labelsFilterPath() {
+ return this.isGroupBoard
+ ? this.labelsFilterBasePath.replace(':project_path', this.projectPathForActiveIssue)
+ : this.labelsFilterBasePath;
},
},
methods: {
@@ -98,21 +110,19 @@ export default {
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
- handleUpdateSelectedLabels(input) {
+ handleUpdateSelectedLabels({ labels, id }) {
this.setActiveBoardItemLabels({
- iid: this.activeBoardItem.iid,
+ id,
projectPath: this.projectPathForActiveIssue,
- addLabelIds: input.map((label) => getIdFromGraphQLId(label.id)),
- removeLabelIds: this.activeBoardItem.labels
- .filter((label) => !input.find((selected) => selected.id === label.id))
- .map((label) => label.id),
+ labelIds: labels.map((label) => getIdFromGraphQLId(label.id)),
+ labels,
});
},
- handleLabelRemove(input) {
+ handleLabelRemove(removeLabelId) {
this.setActiveBoardItemLabels({
iid: this.activeBoardItem.iid,
projectPath: this.projectPathForActiveIssue,
- removeLabelIds: [input],
+ removeLabelIds: [removeLabelId],
});
},
},
@@ -207,14 +217,14 @@ export default {
:full-path="projectPathForActiveIssue"
:allow-label-remove="allowLabelEdit"
:allow-multiselect="true"
- :selected-labels="activeBoardItem.labels"
- :labels-select-in-progress="isSettingLabels"
:footer-create-label-title="createLabelTitle"
:footer-manage-label-title="manageLabelTitle"
:labels-create-title="createLabelTitle"
- :labels-filter-base-path="projectPathForActiveIssue"
+ :labels-filter-base-path="labelsFilterPath"
:attr-workspace-path="attrWorkspacePath"
+ workspace-type="project"
:issuable-type="issuableType"
+ :label-create-type="labelType"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index 7f242dea644..6e6ada2d109 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -1,6 +1,7 @@
<script>
-import { pickBy } from 'lodash';
+import { pickBy, isEmpty } from 'lodash';
import { mapActions } from 'vuex';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -19,6 +20,11 @@ export default {
type: Array,
required: true,
},
+ eeFilters: {
+ required: false,
+ type: Object,
+ default: () => ({}),
+ },
},
data() {
return {
@@ -26,57 +32,6 @@ export default {
};
},
computed: {
- urlParams() {
- const {
- authorUsername,
- labelName,
- assigneeUsername,
- search,
- milestoneTitle,
- types,
- weight,
- } = this.filterParams;
- let notParams = {};
-
- if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
- notParams = pickBy(
- {
- 'not[label_name][]': this.filterParams.not.labelName,
- 'not[author_username]': this.filterParams.not.authorUsername,
- 'not[assignee_username]': this.filterParams.not.assigneeUsername,
- 'not[types]': this.filterParams.not.types,
- 'not[milestone_title]': this.filterParams.not.milestoneTitle,
- 'not[weight]': this.filterParams.not.weight,
- },
- undefined,
- );
- }
-
- return {
- ...notParams,
- author_username: authorUsername,
- 'label_name[]': labelName,
- assignee_username: assigneeUsername,
- milestone_title: milestoneTitle,
- search,
- types,
- weight,
- };
- },
- },
- methods: {
- ...mapActions(['performSearch']),
- handleFilter(filters) {
- this.filterParams = this.getFilterParams(filters);
-
- updateHistory({
- url: setUrlParams(this.urlParams, window.location.href, true, false, true),
- title: document.title,
- replace: true,
- });
-
- this.performSearch();
- },
getFilteredSearchValue() {
const {
authorUsername,
@@ -86,6 +41,8 @@ export default {
milestoneTitle,
types,
weight,
+ epicId,
+ myReactionEmoji,
} = this.filterParams;
const filteredSearchValue = [];
@@ -133,6 +90,20 @@ export default {
});
}
+ if (myReactionEmoji) {
+ filteredSearchValue.push({
+ type: 'my_reaction_emoji',
+ value: { data: myReactionEmoji, operator: '=' },
+ });
+ }
+
+ if (epicId) {
+ filteredSearchValue.push({
+ type: 'epic_id',
+ value: { data: epicId, operator: '=' },
+ });
+ }
+
if (this.filterParams['not[authorUsername]']) {
filteredSearchValue.push({
type: 'author_username',
@@ -177,12 +148,89 @@ export default {
});
}
+ if (this.filterParams['not[epicId]']) {
+ filteredSearchValue.push({
+ type: 'epic_id',
+ value: { data: this.filterParams['not[epicId]'], operator: '!=' },
+ });
+ }
+
+ if (this.filterParams['not[myReactionEmoji]']) {
+ filteredSearchValue.push({
+ type: 'my_reaction_emoji',
+ value: { data: this.filterParams['not[myReactionEmoji]'], operator: '!=' },
+ });
+ }
+
if (search) {
filteredSearchValue.push(search);
}
return filteredSearchValue;
},
+ urlParams() {
+ const {
+ authorUsername,
+ labelName,
+ assigneeUsername,
+ search,
+ milestoneTitle,
+ types,
+ weight,
+ epicId,
+ myReactionEmoji,
+ } = this.filterParams;
+
+ let notParams = {};
+
+ if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
+ notParams = pickBy(
+ {
+ 'not[label_name][]': this.filterParams.not.labelName,
+ 'not[author_username]': this.filterParams.not.authorUsername,
+ 'not[assignee_username]': this.filterParams.not.assigneeUsername,
+ 'not[types]': this.filterParams.not.types,
+ 'not[milestone_title]': this.filterParams.not.milestoneTitle,
+ 'not[weight]': this.filterParams.not.weight,
+ 'not[epic_id]': this.filterParams.not.epicId,
+ 'not[my_reaction_emoji]': this.filterParams.not.myReactionEmoji,
+ },
+ undefined,
+ );
+ }
+
+ return {
+ ...notParams,
+ author_username: authorUsername,
+ 'label_name[]': labelName,
+ assignee_username: assigneeUsername,
+ milestone_title: milestoneTitle,
+ search,
+ types,
+ weight,
+ epic_id: getIdFromGraphQLId(epicId),
+ my_reaction_emoji: myReactionEmoji,
+ };
+ },
+ },
+ created() {
+ if (!isEmpty(this.eeFilters)) {
+ this.filterParams = this.eeFilters;
+ }
+ },
+ methods: {
+ ...mapActions(['performSearch']),
+ handleFilter(filters) {
+ this.filterParams = this.getFilterParams(filters);
+
+ updateHistory({
+ url: setUrlParams(this.urlParams, window.location.href, true, false, true),
+ title: document.title,
+ replace: true,
+ });
+
+ this.performSearch();
+ },
getFilterParams(filters = []) {
const notFilters = filters.filter((item) => item.value.operator === '!=');
const equalsFilters = filters.filter(
@@ -216,6 +264,12 @@ export default {
case 'weight':
filterParams.weight = filter.value.data;
break;
+ case 'epic_id':
+ filterParams.epicId = filter.value.data;
+ break;
+ case 'my_reaction_emoji':
+ filterParams.myReactionEmoji = filter.value.data;
+ break;
case 'filtered-search-term':
if (filter.value.data) plainText.push(filter.value.data);
break;
@@ -243,7 +297,7 @@ export default {
namespace=""
:tokens="tokens"
:search-input-placeholder="$options.i18n.search"
- :initial-filter-value="getFilteredSearchValue()"
+ :initial-filter-value="getFilteredSearchValue"
@onFilter="handleFilter"
/>
</template>
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index e939f0c0ebe..6ad57fd8985 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -2,10 +2,10 @@
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import { TYPE_USER, TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
-import { fullLabelId, fullBoardId } from '../boards_util';
+import { fullLabelId } from '../boards_util';
import { formType } from '../constants';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
@@ -18,11 +18,11 @@ const boardDefaults = {
name: '',
labels: [],
milestone: {},
- iteration_id: undefined,
+ iteration: {},
assignee: {},
weight: null,
- hide_backlog_list: false,
- hide_closed_list: false,
+ hideBacklogList: false,
+ hideClosedList: false,
};
export default {
@@ -57,39 +57,16 @@ export default {
type: Boolean,
required: true,
},
- labelsPath: {
- type: String,
- required: true,
- },
- labelsWebUrl: {
- type: String,
- required: true,
- },
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: false,
default: false,
},
- projectId: {
- type: Number,
- required: false,
- default: 0,
- },
- groupId: {
- type: Number,
- required: false,
- default: 0,
- },
weights: {
type: Array,
required: false,
default: () => [],
},
- enableScopedLabels: {
- type: Boolean,
- required: false,
- default: false,
- },
currentBoard: {
type: Object,
required: true,
@@ -167,17 +144,16 @@ export default {
return destroyBoardMutation;
},
baseMutationVariables() {
- const { board } = this;
- const variables = {
- name: board.name,
- hideBacklogList: board.hide_backlog_list,
- hideClosedList: board.hide_closed_list,
- };
+ const {
+ board: { name, hideBacklogList, hideClosedList, id },
+ } = this;
- return board.id
+ const variables = { name, hideBacklogList, hideClosedList };
+
+ return id
? {
...variables,
- id: fullBoardId(board.id),
+ id,
}
: {
...variables,
@@ -191,11 +167,13 @@ export default {
assigneeId: this.board.assignee?.id
? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
: null,
+ // Temporarily converting to milestone ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779
milestoneId: this.board.milestone?.id
- ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
+ ? convertToGraphQLId(TYPE_MILESTONE, getIdFromGraphQLId(this.board.milestone.id))
: null,
- iterationId: this.board.iteration_id
- ? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id)
+ // Temporarily converting to iteration ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779
+ iterationId: this.board.iteration?.id
+ ? convertToGraphQLId(TYPE_ITERATION, getIdFromGraphQLId(this.board.iteration.id))
: null,
};
},
@@ -249,7 +227,7 @@ export default {
await this.$apollo.mutate({
mutation: this.deleteMutation,
variables: {
- id: fullBoardId(this.board.id),
+ id: this.board.id,
},
});
},
@@ -285,19 +263,12 @@ export default {
}
},
setIteration(iterationId) {
- this.board.iteration_id = iterationId;
+ this.$set(this.board, 'iteration', {
+ id: iterationId,
+ });
},
setBoardLabels(labels) {
- labels.forEach((label) => {
- if (label.set && !this.board.labels.find((l) => l.id === label.id)) {
- this.board.labels.push({
- ...label,
- textColor: label.text_color,
- });
- } else if (!label.set) {
- this.board.labels = this.board.labels.filter((selected) => selected.id !== label.id);
- }
- });
+ this.board.labels = labels;
},
setAssignee(assigneeId) {
this.$set(this.board, 'assignee', {
@@ -361,8 +332,8 @@ export default {
</div>
<board-configuration-options
- :hide-backlog-list.sync="board.hide_backlog_list"
- :hide-closed-list.sync="board.hide_closed_list"
+ :hide-backlog-list.sync="board.hideBacklogList"
+ :hide-closed-list.sync="board.hideClosedList"
:readonly="readonly"
/>
@@ -371,11 +342,6 @@ export default {
:collapse-scope="isNewForm"
:board="board"
:can-admin-board="canAdminBoard"
- :labels-path="labelsPath"
- :labels-web-url="labelsWebUrl"
- :enable-scoped-labels="enableScopedLabels"
- :project-id="projectId"
- :group-id="groupId"
:weights="weights"
@set-iteration="setIteration"
@set-board-labels="setBoardLabels"
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index a8d71ab7a35..e985a368e64 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -15,6 +15,8 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { n__, s__, __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AccessorUtilities from '../../lib/utils/accessor';
import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
@@ -40,7 +42,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [Tracking.mixin()],
+ mixins: [Tracking.mixin(), glFeatureFlagMixin()],
inject: {
boardId: {
default: '',
@@ -86,6 +88,13 @@ export default {
listTitle() {
return this.list?.label?.description || this.list?.assignee?.name || this.list.title || '';
},
+ listIterationPeriod() {
+ const iteration = this.list?.iteration;
+ return iteration ? this.getIterationPeriod(iteration) : '';
+ },
+ isIterationList() {
+ return this.listType === ListType.iteration;
+ },
showListHeaderButton() {
return !this.disabled && this.listType !== ListType.closed;
},
@@ -96,7 +105,10 @@ export default {
return this.listType === ListType.assignee && this.showListDetails;
},
showIterationListDetails() {
- return this.listType === ListType.iteration && this.showListDetails;
+ return this.isIterationList && this.showListDetails;
+ },
+ iterationCadencesAvailable() {
+ return this.isIterationList && this.glFeatures.iterationCadences;
},
showListDetails() {
return !this.list.collapsed || !this.isSwimlanesHeader;
@@ -208,6 +220,16 @@ export default {
updateListFunction() {
this.updateList({ listId: this.list.id, collapsed: this.list.collapsed });
},
+ /**
+ * TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/344619
+ * This method also exists as a utility function in ee/../iterations/utils.js
+ * Remove the duplication when the EE code is separated from this compoment.
+ */
+ getIterationPeriod({ startDate, dueDate }) {
+ const start = formatDate(startDate, 'mmm d, yyyy', true);
+ const due = formatDate(dueDate, 'mmm d, yyyy', true);
+ return `${start} - ${due}`;
+ },
},
};
</script>
@@ -307,6 +329,13 @@ export default {
class="board-title-main-text gl-text-truncate"
>
{{ listTitle }}
+ <span
+ v-if="iterationCadencesAvailable"
+ class="gl-display-inline-block gl-text-gray-400"
+ data-testid="board-list-iteration-period"
+ >
+ {{ listIterationPeriod }}</span
+ >
</span>
<span
v-if="listType === 'assignee'"
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 98027917221..71facba1378 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -9,17 +9,20 @@ import {
GlModalDirective,
} from '@gitlab/ui';
import { throttle } from 'lodash';
-import { mapGetters, mapState } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
+import { s__ } from '~/locale';
import eventHub from '../eventhub';
-import groupQuery from '../graphql/group_boards.query.graphql';
-import projectQuery from '../graphql/project_boards.query.graphql';
+import groupBoardsQuery from '../graphql/group_boards.query.graphql';
+import projectBoardsQuery from '../graphql/project_boards.query.graphql';
+import groupBoardQuery from '../graphql/group_board.query.graphql';
+import projectBoardQuery from '../graphql/project_board.query.graphql';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
@@ -39,10 +42,6 @@ export default {
},
inject: ['fullPath', 'recentBoardsEndpoint'],
props: {
- currentBoard: {
- type: Object,
- required: true,
- },
throttleDuration: {
type: Number,
default: 200,
@@ -64,22 +63,6 @@ export default {
type: Boolean,
required: true,
},
- labelsPath: {
- type: String,
- required: true,
- },
- labelsWebUrl: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- groupId: {
- type: Number,
- required: true,
- },
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: true,
@@ -88,11 +71,6 @@ export default {
type: Array,
required: true,
},
- enabledScopedLabels: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
@@ -107,14 +85,47 @@ export default {
maxPosition: 0,
filterTerm: '',
currentPage: '',
+ board: {},
};
},
+ apollo: {
+ board: {
+ query() {
+ return this.currentBoardQuery;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ boardId: this.fullBoardId,
+ };
+ },
+ update(data) {
+ const board = data.workspace?.board;
+ return {
+ ...board,
+ labels: board?.labels?.nodes,
+ };
+ },
+ error() {
+ this.setError({ message: this.$options.i18n.errorFetchingBoard });
+ },
+ },
+ },
computed: {
- ...mapState(['boardType']),
- ...mapGetters(['isGroupBoard']),
+ ...mapState(['boardType', 'fullBoardId']),
+ ...mapGetters(['isGroupBoard', 'isProjectBoard']),
parentType() {
return this.boardType;
},
+ currentBoardQueryCE() {
+ return this.isGroupBoard ? groupBoardQuery : projectBoardQuery;
+ },
+ currentBoardQuery() {
+ return this.currentBoardQueryCE;
+ },
+ isBoardLoading() {
+ return this.$apollo.queries.board.loading;
+ },
loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards);
},
@@ -123,9 +134,6 @@ export default {
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
);
},
- board() {
- return this.currentBoard;
- },
showCreate() {
return this.multipleIssueBoardsAvailable;
},
@@ -158,6 +166,7 @@ export default {
eventHub.$off('showBoardModal', this.showPage);
},
methods: {
+ ...mapActions(['setError']),
showPage(page) {
this.currentPage = page;
},
@@ -174,7 +183,7 @@ export default {
}));
},
boardQuery() {
- return this.isGroupBoard ? groupQuery : projectQuery;
+ return this.isGroupBoard ? groupBoardsQuery : projectBoardsQuery;
},
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
@@ -250,6 +259,9 @@ export default {
this.hasScrollFade = this.isScrolledUp();
},
},
+ i18n: {
+ errorFetchingBoard: s__('Board|An error occurred while fetching the board, please try again.'),
+ },
};
</script>
@@ -260,6 +272,7 @@ export default {
data-qa-selector="boards_dropdown"
toggle-class="dropdown-menu-toggle js-dropdown-toggle"
menu-class="flex-column dropdown-extended-height"
+ :loading="isBoardLoading"
:text="board.name"
@show="loadBoards"
>
@@ -354,15 +367,10 @@ export default {
<board-form
v-if="currentPage"
- :labels-path="labelsPath"
- :labels-web-url="labelsWebUrl"
- :project-id="projectId"
- :group-id="groupId"
:can-admin-board="canAdminBoard"
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights"
- :enable-scoped-labels="enabledScopedLabels"
- :current-board="currentBoard"
+ :current-board="board"
:current-page="currentPage"
@cancel="cancel"
/>
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
index b6c5ef955c6..bdb9c2be836 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -1,13 +1,20 @@
<script>
import { GlFilteredSearchToken } from '@gitlab/ui';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { mapActions } from 'vuex';
-import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
+import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue';
+import { BoardType } from '~/boards/constants';
+import axios from '~/lib/utils/axios_utils';
import issueBoardFilters from '~/boards/issue_board_filters';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
-import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ DEFAULT_MILESTONES_GRAPHQL,
+ TOKEN_TITLE_MY_REACTION,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
@@ -19,6 +26,7 @@ export default {
},
i18n: {
search: __('Search'),
+ epic: __('Epic'),
label: __('Label'),
author: __('Author'),
assignee: __('Assignee'),
@@ -31,6 +39,7 @@ export default {
isNot: __('is not'),
},
components: { BoardFilteredSearch },
+ inject: ['isSignedIn'],
props: {
fullPath: {
type: String,
@@ -42,7 +51,15 @@ export default {
},
},
computed: {
- tokens() {
+ isGroupBoard() {
+ return this.boardType === BoardType.group;
+ },
+ epicsGroupPath() {
+ return this.isGroupBoard
+ ? this.fullPath
+ : this.fullPath.slice(0, this.fullPath.lastIndexOf('/'));
+ },
+ tokensCE() {
const {
label,
is,
@@ -103,6 +120,32 @@ export default {
symbol: '~',
fetchLabels,
},
+ ...(this.isSignedIn
+ ? [
+ {
+ type: 'my_reaction_emoji',
+ title: TOKEN_TITLE_MY_REACTION,
+ icon: 'thumb-up',
+ token: EmojiToken,
+ unique: true,
+ fetchEmojis: (search = '') => {
+ // TODO: Switch to GraphQL query when backend is ready: https://gitlab.com/gitlab-org/gitlab/-/issues/339694
+ return axios
+ .get(`${gon.relative_url_root || ''}/-/autocomplete/award_emojis`)
+ .then(({ data }) => {
+ if (search) {
+ return {
+ data: fuzzaldrinPlus.filter(data, search, {
+ key: ['name'],
+ }),
+ };
+ }
+ return { data };
+ });
+ },
+ },
+ ]
+ : []),
{
type: 'milestone_title',
title: milestone,
@@ -117,7 +160,6 @@ export default {
icon: 'issues',
title: type,
type: 'types',
- operators: [{ value: '=', description: is }],
token: GlFilteredSearchToken,
unique: true,
options: [
@@ -134,6 +176,9 @@ export default {
},
];
},
+ tokens() {
+ return this.tokensCE;
+ },
},
methods: {
...mapActions(['fetchMilestones']),
diff --git a/app/assets/javascripts/boards/components/new_board_button.vue b/app/assets/javascripts/boards/components/new_board_button.vue
new file mode 100644
index 00000000000..f7914c636cc
--- /dev/null
+++ b/app/assets/javascripts/boards/components/new_board_button.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { formType } from '~/boards/constants';
+import eventHub from '~/boards/eventhub';
+import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
+
+export default {
+ components: {
+ GlButton,
+ GitlabExperiment,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ mixins: [Tracking.mixin()],
+ inject: ['multipleIssueBoardsAvailable', 'canAdminBoard'],
+ computed: {
+ canShowCreateButton() {
+ return this.canAdminBoard && this.multipleIssueBoardsAvailable;
+ },
+ createButtonText() {
+ return s__('Boards|New board');
+ },
+ },
+ methods: {
+ showDialog() {
+ this.track('click_button', { label: 'create_board' });
+ eventHub.$emit('showBoardModal', formType.new);
+ },
+ },
+};
+</script>
+
+<template>
+ <gitlab-experiment name="prominent_create_board_btn">
+ <template #control> </template>
+ <template #candidate>
+ <div v-if="canShowCreateButton" class="gl-ml-1 gl-mr-3 gl-display-flex gl-align-items-center">
+ <gl-button data-qa-selector="new_board_button" @click.prevent="showDialog">
+ {{ createButtonText }}
+ </gl-button>
+ </div>
+ </template>
+ </gitlab-experiment>
+</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
index e74463825c5..ec53947fd5f 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
@@ -91,9 +91,7 @@ export default {
try {
const addLabelIds = payload.filter((label) => label.set).map((label) => label.id);
- const removeLabelIds = this.selectedLabels
- .filter((label) => !payload.find((selected) => selected.id === label.id))
- .map((label) => label.id);
+ const removeLabelIds = payload.filter((label) => !label.set).map((label) => label.id);
const input = {
addLabelIds,
@@ -164,7 +162,7 @@ export default {
:labels-list-title="__('Select label')"
:dropdown-button-text="__('Choose labels')"
:is-editing="edit"
- variant="embedded"
+ variant="sidebar"
class="gl-display-block labels gl-w-full"
@updateSelectedLabels="setLabels"
>
diff --git a/app/assets/javascripts/boards/graphql.js b/app/assets/javascripts/boards/graphql.js
index d8d16184936..64938cb42ed 100644
--- a/app/assets/javascripts/boards/graphql.js
+++ b/app/assets/javascripts/boards/graphql.js
@@ -17,6 +17,5 @@ export const gqlClient = createDefaultClient(
fragmentMatcher,
},
- assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
index 3eb23f62940..0e1d11727cf 100644
--- a/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
@@ -1,6 +1,6 @@
#import "./board_list.fragment.graphql"
-mutation CreateBoardList($boardId: BoardID!, $backlog: Boolean, $labelId: LabelID) {
+mutation createBoardList($boardId: BoardID!, $backlog: Boolean, $labelId: LabelID) {
boardListCreate(input: { boardId: $boardId, backlog: $backlog, labelId: $labelId }) {
list {
...BoardListFragment
diff --git a/app/assets/javascripts/boards/graphql/board_lists.query.graphql b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
index 734867c77e9..47e87907d76 100644
--- a/app/assets/javascripts/boards/graphql/board_lists.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
@@ -1,6 +1,6 @@
#import "ee_else_ce/boards/graphql/board_list.fragment.graphql"
-query ListIssues(
+query BoardLists(
$fullPath: ID!
$boardId: ID!
$filters: BoardIssueInput
diff --git a/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql b/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql
new file mode 100644
index 00000000000..57f51822d91
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql
@@ -0,0 +1,6 @@
+fragment BoardScopeFragment on Board {
+ id
+ name
+ hideBacklogList
+ hideClosedList
+}
diff --git a/app/assets/javascripts/boards/graphql/group_board.query.graphql b/app/assets/javascripts/boards/graphql/group_board.query.graphql
new file mode 100644
index 00000000000..77c8e0378f0
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/group_board.query.graphql
@@ -0,0 +1,9 @@
+#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql"
+
+query GroupBoard($fullPath: ID!, $boardId: ID!) {
+ workspace: group(fullPath: $fullPath) {
+ board(id: $boardId) {
+ ...BoardScopeFragment
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql b/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql
deleted file mode 100644
index 1c382c4747b..00000000000
--- a/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-query GroupBoardIterations($fullPath: ID!, $title: String) {
- group(fullPath: $fullPath) {
- iterations(includeAncestors: true, title: $title) {
- nodes {
- id
- title
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/graphql/group_projects.query.graphql b/app/assets/javascripts/boards/graphql/group_projects.query.graphql
index 3218c06357c..c5732bbaff3 100644
--- a/app/assets/javascripts/boards/graphql/group_projects.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_projects.query.graphql
@@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
-query getGroupProjects($fullPath: ID!, $search: String, $after: String) {
+query boardsGetGroupProjects($fullPath: ID!, $search: String, $after: String) {
group(fullPath: $fullPath) {
projects(search: $search, after: $after, first: 100, includeSubgroups: true) {
nodes {
diff --git a/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
index 3c574fd8c87..570731ecac6 100644
--- a/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
@@ -1,6 +1,6 @@
#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
-mutation IssueMoveList(
+mutation issueMoveList(
$projectPath: ID!
$iid: String!
$boardId: ID!
diff --git a/app/assets/javascripts/boards/graphql/lists_issues.query.graphql b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
index 787dd77b901..9f93bc6d5bf 100644
--- a/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
+++ b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
@@ -1,6 +1,6 @@
#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
-query ListIssues(
+query BoardListEE(
$fullPath: ID!
$boardId: ID!
$id: ID
diff --git a/app/assets/javascripts/boards/graphql/project_board.query.graphql b/app/assets/javascripts/boards/graphql/project_board.query.graphql
new file mode 100644
index 00000000000..6e4cd6bed57
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/project_board.query.graphql
@@ -0,0 +1,9 @@
+#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql"
+
+query ProjectBoard($fullPath: ID!, $boardId: ID!) {
+ workspace: project(fullPath: $fullPath) {
+ board(id: $boardId) {
+ ...BoardScopeFragment
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql b/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql
deleted file mode 100644
index 078151a275a..00000000000
--- a/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-query ProjectBoardIterations($fullPath: ID!, $title: String) {
- project(fullPath: $fullPath) {
- iterations(includeAncestors: true, title: $title) {
- nodes {
- id
- title
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/graphql/project_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
index 724b7f5a34c..61c9ddded9b 100644
--- a/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
@@ -1,4 +1,4 @@
-query projectMilestones(
+query boardProjectMilestones(
$fullPath: ID!
$state: MilestoneStateEnum
$includeAncestors: Boolean
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index b6b1094fb3a..6fa8dd63245 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -13,9 +13,10 @@ import FilteredSearchBoards from '~/boards/filtered_search_boards';
import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards';
import store from '~/boards/stores';
import toggleFocusMode from '~/boards/toggle_focus';
-import { NavigationType, parseBoolean } from '~/lib/utils/common_utils';
+import { NavigationType, isLoggedIn, parseBoolean } from '~/lib/utils/common_utils';
import { fullBoardId } from './boards_util';
import boardConfigToggle from './config_toggle';
+import initNewBoard from './new_board';
import { gqlClient } from './graphql';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
@@ -109,7 +110,7 @@ export default () => {
});
if (gon?.features?.issueBoardsFilteredSearch) {
- initBoardsFilteredSearch(apolloProvider);
+ initBoardsFilteredSearch(apolloProvider, isLoggedIn());
}
mountBoardApp($boardApp);
@@ -130,6 +131,7 @@ export default () => {
}
boardConfigToggle();
+ initNewBoard();
toggleFocusMode();
toggleLabels();
@@ -142,5 +144,7 @@ export default () => {
fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
+ allowScopedLabels: $boardApp.dataset.scopedLabels,
+ labelsManagePath: $boardApp.dataset.labelsManagePath,
});
};
diff --git a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
index 7732091ef34..1ea74d5685c 100644
--- a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
+++ b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
-import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue';
+import IssueBoardFilteredSearch from 'ee_else_ce/boards/components/issue_board_filtered_search.vue';
import store from '~/boards/stores';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
-export default (apolloProvider) => {
+export default (apolloProvider, isSignedIn) => {
const el = document.getElementById('js-issue-board-filtered-search');
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
@@ -20,6 +20,7 @@ export default (apolloProvider) => {
el,
provide: {
initialFilterParams,
+ isSignedIn,
},
store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094
apolloProvider,
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index a3a8ad06c43..ed32579a9c3 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -1,23 +1,32 @@
+import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
import store from '~/boards/stores';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
+import introspectionQueryResultData from '~/sidebar/fragmentTypes.json';
Vue.use(VueApollo);
+const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData,
+});
+
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
- assumeImmutableResults: true,
+ cacheConfig: {
+ fragmentMatcher,
+ },
},
),
});
export default (params = {}) => {
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
+ const { dataset } = boardsSwitcherElement;
return new Vue({
el: boardsSwitcherElement,
components: {
@@ -29,18 +38,16 @@ export default (params = {}) => {
fullPath: params.fullPath,
rootPath: params.rootPath,
recentBoardsEndpoint: params.recentBoardsEndpoint,
+ allowScopedLabels: params.allowScopedLabels,
+ labelsManagePath: params.labelsManagePath,
+ allowLabelCreate: parseBoolean(dataset.canAdminBoard),
},
data() {
- const { dataset } = boardsSwitcherElement;
-
const boardsSelectorProps = {
...dataset,
- currentBoard: JSON.parse(dataset.currentBoard),
hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
canAdminBoard: parseBoolean(dataset.canAdminBoard),
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
- projectId: dataset.projectId ? Number(dataset.projectId) : 0,
- groupId: Number(dataset.groupId),
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
weights: JSON.parse(dataset.weights),
};
diff --git a/app/assets/javascripts/boards/new_board.js b/app/assets/javascripts/boards/new_board.js
new file mode 100644
index 00000000000..34f2fea79a9
--- /dev/null
+++ b/app/assets/javascripts/boards/new_board.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { getExperimentVariant } from '~/experimentation/utils';
+import { CANDIDATE_VARIANT } from '~/experimentation/constants';
+import NewBoardButton from './components/new_board_button.vue';
+
+export default () => {
+ if (getExperimentVariant('prominent_create_board_btn') !== CANDIDATE_VARIANT) {
+ return;
+ }
+
+ const el = document.querySelector('.js-new-board');
+
+ if (!el) {
+ return;
+ }
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ provide: {
+ multipleIssueBoardsAvailable: parseBoolean(el.dataset.multipleIssueBoardsAvailable),
+ canAdminBoard: parseBoolean(el.dataset.canAdminBoard),
+ },
+ render(h) {
+ return h(NewBoardButton);
+ },
+ });
+};
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index ca993e75cf9..3a96e535cf7 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -36,13 +36,11 @@ import {
} from '../boards_util';
import { gqlClient } from '../graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
-import groupBoardIterationsQuery from '../graphql/group_board_iterations.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
-import projectBoardIterationsQuery from '../graphql/project_board_iterations.query.graphql';
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
import * as types from './mutation_types';
@@ -203,52 +201,6 @@ export default {
});
},
- fetchIterations({ state, commit }, title) {
- commit(types.RECEIVE_ITERATIONS_REQUEST);
-
- const { fullPath, boardType } = state;
-
- const variables = {
- fullPath,
- title,
- };
-
- let query;
- if (boardType === BoardType.project) {
- query = projectBoardIterationsQuery;
- }
- if (boardType === BoardType.group) {
- query = groupBoardIterationsQuery;
- }
-
- if (!query) {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('Unknown board type');
- }
-
- return gqlClient
- .query({
- query,
- variables,
- })
- .then(({ data }) => {
- const errors = data[boardType]?.errors;
- const iterations = data[boardType]?.iterations.nodes;
-
- if (errors?.[0]) {
- throw new Error(errors[0]);
- }
-
- commit(types.RECEIVE_ITERATIONS_SUCCESS, iterations);
-
- return iterations;
- })
- .catch((e) => {
- commit(types.RECEIVE_ITERATIONS_FAILURE);
- throw e;
- });
- },
-
fetchMilestones({ state, commit }, searchTerm) {
commit(types.RECEIVE_MILESTONES_REQUEST);
@@ -656,30 +608,45 @@ export default {
},
setActiveIssueLabels: async ({ commit, getters }, input) => {
- commit(types.SET_LABELS_LOADING, true);
const { activeBoardItem } = getters;
- const { data } = await gqlClient.mutate({
- mutation: issueSetLabelsMutation,
- variables: {
- input: {
- iid: input.iid || String(activeBoardItem.iid),
- addLabelIds: input.addLabelIds ?? [],
- removeLabelIds: input.removeLabelIds ?? [],
- projectPath: input.projectPath,
+
+ if (!gon.features?.labelsWidget) {
+ const { data } = await gqlClient.mutate({
+ mutation: issueSetLabelsMutation,
+ variables: {
+ input: {
+ iid: input.iid || String(activeBoardItem.iid),
+ labelIds: input.labelsId ?? undefined,
+ addLabelIds: input.addLabelIds ?? [],
+ removeLabelIds: input.removeLabelIds ?? [],
+ projectPath: input.projectPath,
+ },
},
- },
- });
+ });
- commit(types.SET_LABELS_LOADING, false);
+ if (data.updateIssue?.errors?.length > 0) {
+ throw new Error(data.updateIssue.errors);
+ }
+
+ commit(types.UPDATE_BOARD_ITEM_BY_ID, {
+ itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
+ prop: 'labels',
+ value: data.updateIssue?.issue?.labels.nodes,
+ });
- if (data.updateIssue?.errors?.length > 0) {
- throw new Error(data.updateIssue.errors);
+ return;
}
+ let labels = input?.labels || [];
+ if (input.removeLabelIds) {
+ labels = activeBoardItem.labels.filter(
+ (label) => input.removeLabelIds[0] !== getIdFromGraphQLId(label.id),
+ );
+ }
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
+ itemId: input.id || activeBoardItem.id,
prop: 'labels',
- value: data.updateIssue.issue.labels.nodes,
+ value: labels,
});
},
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 26b785932bb..31b78014525 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -28,7 +28,6 @@ export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST';
export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID';
-export const SET_LABELS_LOADING = 'SET_LABELS_LOADING';
export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING';
export const RESET_ISSUES = 'RESET_ISSUES';
export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
@@ -42,7 +41,3 @@ export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION';
export const SET_ERROR = 'SET_ERROR';
-
-export const RECEIVE_ITERATIONS_REQUEST = 'RECEIVE_ITERATIONS_REQUEST';
-export const RECEIVE_ITERATIONS_SUCCESS = 'RECEIVE_ITERATIONS_SUCCESS';
-export const RECEIVE_ITERATIONS_FAILURE = 'RECEIVE_ITERATIONS_FAILURE';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index d381c076c19..2a2ce7652e6 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -64,20 +64,6 @@ export default {
);
},
- [mutationTypes.RECEIVE_ITERATIONS_REQUEST](state) {
- state.iterationsLoading = true;
- },
-
- [mutationTypes.RECEIVE_ITERATIONS_SUCCESS](state, iterations) {
- state.iterations = iterations;
- state.iterationsLoading = false;
- },
-
- [mutationTypes.RECEIVE_ITERATIONS_FAILURE](state) {
- state.iterationsLoading = false;
- state.error = __('Failed to load iterations.');
- },
-
[mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
state.activeId = id;
state.sidebarType = sidebarType;
@@ -195,10 +181,6 @@ export default {
Vue.set(state.boardItems[itemId], prop, value);
},
- [mutationTypes.SET_LABELS_LOADING](state, isLoading) {
- state.isSettingLabels = isLoading;
- },
-
[mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
state.isSettingAssignees = isLoading;
},
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 2a6605e687b..80c51c966d2 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -12,7 +12,6 @@ export default () => ({
listsFlags: {},
boardItemsByListId: {},
backupItemsList: [],
- isSettingLabels: false,
isSettingAssignees: false,
pageInfoByListId: {},
boardItems: {},
diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js
index b8b8a0b2867..b9d3742974c 100644
--- a/app/assets/javascripts/breadcrumb.js
+++ b/app/assets/javascripts/breadcrumb.js
@@ -1,5 +1,4 @@
import $ from 'jquery';
-import { hide } from '~/tooltips';
export const addTooltipToEl = (el) => {
const textEl = el.querySelector('.js-breadcrumb-item-text');
@@ -19,16 +18,23 @@ export default () => {
.filter((el) => !el.classList.contains('dropdown'))
.map((el) => el.querySelector('a'))
.filter((el) => el);
- const $expander = $('.js-breadcrumbs-collapsed-expander');
+ const $expanderBtn = $('.js-breadcrumbs-collapsed-expander');
topLevelLinks.forEach((el) => addTooltipToEl(el));
- $expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', (e) => {
- const $el = $('.js-breadcrumbs-collapsed-expander', e.currentTarget);
+ $expanderBtn.on('click', () => {
+ const detailItems = $('.breadcrumbs-detail-item');
+ const hiddenClass = 'gl-display-none!';
- $el.toggleClass('open');
+ $.each(detailItems, (_key, item) => {
+ $(item).toggleClass(hiddenClass);
+ });
- hide($el);
+ // remove the ellipsis
+ $('li.expander').remove();
+
+ // set focus on first breadcrumb item
+ $('.breadcrumb-item-text').first().focus();
});
}
};
diff --git a/app/assets/javascripts/chronic_duration.js b/app/assets/javascripts/chronic_duration.js
new file mode 100644
index 00000000000..1073d736b06
--- /dev/null
+++ b/app/assets/javascripts/chronic_duration.js
@@ -0,0 +1,417 @@
+/*
+ * NOTE:
+ * Changes to this file should be kept in sync with
+ * https://gitlab.com/gitlab-org/gitlab-chronic-duration/-/blob/master/lib/gitlab_chronic_duration.rb.
+ */
+
+/*
+ * This code is based on code from
+ * https://gitlab.com/gitlab-org/gitlab-chronic-duration and is
+ * distributed under the following license:
+ *
+ * MIT License
+ *
+ * Copyright (c) Henry Poydar
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+export class DurationParseError extends Error {}
+
+// On average, there's a little over 4 weeks in month.
+const FULL_WEEKS_PER_MONTH = 4;
+
+const HOURS_PER_DAY = 24;
+const DAYS_PER_MONTH = 30;
+
+const FLOAT_MATCHER = /[0-9]*\.?[0-9]+/g;
+const DURATION_UNITS_LIST = ['seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years'];
+
+const MAPPINGS = {
+ seconds: 'seconds',
+ second: 'seconds',
+ secs: 'seconds',
+ sec: 'seconds',
+ s: 'seconds',
+ minutes: 'minutes',
+ minute: 'minutes',
+ mins: 'minutes',
+ min: 'minutes',
+ m: 'minutes',
+ hours: 'hours',
+ hour: 'hours',
+ hrs: 'hours',
+ hr: 'hours',
+ h: 'hours',
+ days: 'days',
+ day: 'days',
+ dy: 'days',
+ d: 'days',
+ weeks: 'weeks',
+ week: 'weeks',
+ wks: 'weeks',
+ wk: 'weeks',
+ w: 'weeks',
+ months: 'months',
+ mo: 'months',
+ mos: 'months',
+ month: 'months',
+ years: 'years',
+ year: 'years',
+ yrs: 'years',
+ yr: 'years',
+ y: 'years',
+};
+
+const JOIN_WORDS = ['and', 'with', 'plus'];
+
+function convertToNumber(string) {
+ const f = parseFloat(string);
+ return f % 1 > 0 ? f : parseInt(string, 10);
+}
+
+function durationUnitsSecondsMultiplier(unit, opts) {
+ if (!DURATION_UNITS_LIST.includes(unit)) {
+ return 0;
+ }
+
+ const hoursPerDay = opts.hoursPerDay || HOURS_PER_DAY;
+ const daysPerMonth = opts.daysPerMonth || DAYS_PER_MONTH;
+ const daysPerWeek = Math.trunc(daysPerMonth / FULL_WEEKS_PER_MONTH);
+
+ switch (unit) {
+ case 'years':
+ return 31557600;
+ case 'months':
+ return 3600 * hoursPerDay * daysPerMonth;
+ case 'weeks':
+ return 3600 * hoursPerDay * daysPerWeek;
+ case 'days':
+ return 3600 * hoursPerDay;
+ case 'hours':
+ return 3600;
+ case 'minutes':
+ return 60;
+ case 'seconds':
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+function calculateFromWords(string, opts) {
+ let val = 0;
+ const words = string.split(' ');
+ words.forEach((v, k) => {
+ if (v === '') {
+ return;
+ }
+ if (v.search(FLOAT_MATCHER) >= 0) {
+ val +=
+ convertToNumber(v) *
+ durationUnitsSecondsMultiplier(
+ words[parseInt(k, 10) + 1] || opts.defaultUnit || 'seconds',
+ opts,
+ );
+ }
+ });
+ return val;
+}
+
+// Parse 3:41:59 and return 3 hours 41 minutes 59 seconds
+function filterByType(string) {
+ const chronoUnitsList = DURATION_UNITS_LIST.filter((v) => v !== 'weeks');
+ if (
+ string
+ .replace(/ +/g, '')
+ .search(RegExp(`${FLOAT_MATCHER.source}(:${FLOAT_MATCHER.source})+`, 'g')) >= 0
+ ) {
+ const res = [];
+ string
+ .replace(/ +/g, '')
+ .split(':')
+ .reverse()
+ .forEach((v, k) => {
+ if (!chronoUnitsList[k]) {
+ return;
+ }
+ res.push(`${v} ${chronoUnitsList[k]}`);
+ });
+ return res.reverse().join(' ');
+ }
+ return string;
+}
+
+// Get rid of unknown words and map found
+// words to defined time units
+function filterThroughWhiteList(string, opts) {
+ const res = [];
+ string.split(' ').forEach((word) => {
+ if (word === '') {
+ return;
+ }
+ if (word.search(FLOAT_MATCHER) >= 0) {
+ res.push(word.trim());
+ return;
+ }
+ const strippedWord = word.trim().replace(/^,/g, '').replace(/,$/g, '');
+ if (MAPPINGS[strippedWord] !== undefined) {
+ res.push(MAPPINGS[strippedWord]);
+ } else if (!JOIN_WORDS.includes(strippedWord) && opts.raiseExceptions) {
+ throw new DurationParseError(
+ `An invalid word ${JSON.stringify(word)} was used in the string to be parsed.`,
+ );
+ }
+ });
+ // add '1' at front if string starts with something recognizable but not with a number, like 'day' or 'minute 30sec'
+ if (res.length > 0 && MAPPINGS[res[0]]) {
+ res.splice(0, 0, 1);
+ }
+ return res.join(' ');
+}
+
+function cleanup(string, opts) {
+ let res = string.toLowerCase();
+ /*
+ * TODO The Ruby implementation of this algorithm uses the Numerizer module,
+ * which converts strings like "forty two" to "42", but there is no
+ * JavaScript equivalent of Numerizer. Skip it for now until Numerizer is
+ * ported to JavaScript.
+ */
+ res = filterByType(res);
+ res = res
+ .replace(FLOAT_MATCHER, (n) => ` ${n} `)
+ .replace(/ +/g, ' ')
+ .trim();
+ return filterThroughWhiteList(res, opts);
+}
+
+function humanizeTimeUnit(number, unit, pluralize, keepZero) {
+ if (number === '0' && !keepZero) {
+ return null;
+ }
+ let res = number + unit;
+ // A poor man's pluralizer
+ if (number !== '1' && pluralize) {
+ res += 's';
+ }
+ return res;
+}
+
+// Given a string representation of elapsed time,
+// return an integer (or float, if fractions of a
+// second are input)
+export function parseChronicDuration(string, opts = {}) {
+ const result = calculateFromWords(cleanup(string, opts), opts);
+ return !opts.keepZero && result === 0 ? null : result;
+}
+
+// Given an integer and an optional format,
+// returns a formatted string representing elapsed time
+export function outputChronicDuration(seconds, opts = {}) {
+ const units = {
+ years: 0,
+ months: 0,
+ weeks: 0,
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ seconds,
+ };
+
+ const hoursPerDay = opts.hoursPerDay || HOURS_PER_DAY;
+ const daysPerMonth = opts.daysPerMonth || DAYS_PER_MONTH;
+ const daysPerWeek = Math.trunc(daysPerMonth / FULL_WEEKS_PER_MONTH);
+
+ const decimalPlaces =
+ seconds % 1 !== 0 ? seconds.toString().split('.').reverse()[0].length : null;
+
+ const minute = 60;
+ const hour = 60 * minute;
+ const day = hoursPerDay * hour;
+ const month = daysPerMonth * day;
+ const year = 31557600;
+
+ if (units.seconds >= 31557600 && units.seconds % year < units.seconds % month) {
+ units.years = Math.trunc(units.seconds / year);
+ units.months = Math.trunc((units.seconds % year) / month);
+ units.days = Math.trunc(((units.seconds % year) % month) / day);
+ units.hours = Math.trunc((((units.seconds % year) % month) % day) / hour);
+ units.minutes = Math.trunc(((((units.seconds % year) % month) % day) % hour) / minute);
+ units.seconds = Math.trunc(((((units.seconds % year) % month) % day) % hour) % minute);
+ } else if (seconds >= 60) {
+ units.minutes = Math.trunc(seconds / 60);
+ units.seconds %= 60;
+ if (units.minutes >= 60) {
+ units.hours = Math.trunc(units.minutes / 60);
+ units.minutes = Math.trunc(units.minutes % 60);
+ if (!opts.limitToHours) {
+ if (units.hours >= hoursPerDay) {
+ units.days = Math.trunc(units.hours / hoursPerDay);
+ units.hours = Math.trunc(units.hours % hoursPerDay);
+ if (opts.weeks) {
+ if (units.days >= daysPerWeek) {
+ units.weeks = Math.trunc(units.days / daysPerWeek);
+ units.days = Math.trunc(units.days % daysPerWeek);
+ if (units.weeks >= FULL_WEEKS_PER_MONTH) {
+ units.months = Math.trunc(units.weeks / FULL_WEEKS_PER_MONTH);
+ units.weeks = Math.trunc(units.weeks % FULL_WEEKS_PER_MONTH);
+ }
+ }
+ } else if (units.days >= daysPerMonth) {
+ units.months = Math.trunc(units.days / daysPerMonth);
+ units.days = Math.trunc(units.days % daysPerMonth);
+ }
+ }
+ }
+ }
+ }
+
+ let joiner = opts.joiner || ' ';
+ let process = null;
+
+ let dividers;
+ switch (opts.format) {
+ case 'micro':
+ dividers = {
+ years: 'y',
+ months: 'mo',
+ weeks: 'w',
+ days: 'd',
+ hours: 'h',
+ minutes: 'm',
+ seconds: 's',
+ };
+ joiner = '';
+ break;
+ case 'short':
+ dividers = {
+ years: 'y',
+ months: 'mo',
+ weeks: 'w',
+ days: 'd',
+ hours: 'h',
+ minutes: 'm',
+ seconds: 's',
+ };
+ break;
+ case 'long':
+ dividers = {
+ /* eslint-disable @gitlab/require-i18n-strings */
+ years: ' year',
+ months: ' month',
+ weeks: ' week',
+ days: ' day',
+ hours: ' hour',
+ minutes: ' minute',
+ seconds: ' second',
+ /* eslint-enable @gitlab/require-i18n-strings */
+ pluralize: true,
+ };
+ break;
+ case 'chrono':
+ dividers = {
+ years: ':',
+ months: ':',
+ weeks: ':',
+ days: ':',
+ hours: ':',
+ minutes: ':',
+ seconds: ':',
+ keepZero: true,
+ };
+ process = (str) => {
+ // Pad zeros
+ // Get rid of lead off times if they are zero
+ // Get rid of lead off zero
+ // Get rid of trailing:
+ const divider = ':';
+ const processed = [];
+ str.split(divider).forEach((n) => {
+ if (n === '') {
+ return;
+ }
+ // add zeros only if n is an integer
+ if (n.search('\\.') >= 0) {
+ processed.push(
+ parseFloat(n)
+ .toFixed(decimalPlaces)
+ .padStart(3 + decimalPlaces, '0'),
+ );
+ } else {
+ processed.push(n.padStart(2, '0'));
+ }
+ });
+ return processed
+ .join(divider)
+ .replace(/^(00:)+/g, '')
+ .replace(/^0/g, '')
+ .replace(/:$/g, '');
+ };
+ joiner = '';
+ break;
+ default:
+ dividers = {
+ /* eslint-disable @gitlab/require-i18n-strings */
+ years: ' yr',
+ months: ' mo',
+ weeks: ' wk',
+ days: ' day',
+ hours: ' hr',
+ minutes: ' min',
+ seconds: ' sec',
+ /* eslint-enable @gitlab/require-i18n-strings */
+ pluralize: true,
+ };
+ break;
+ }
+
+ let result = [];
+ ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'].forEach((t) => {
+ if (t === 'weeks' && !opts.weeks) {
+ return;
+ }
+ let num = units[t];
+ if (t === 'seconds' && num % 0 !== 0) {
+ num = num.toFixed(decimalPlaces);
+ } else {
+ num = num.toString();
+ }
+ const keepZero = !dividers.keepZero && t === 'seconds' ? opts.keepZero : dividers.keepZero;
+ const humanized = humanizeTimeUnit(num, dividers[t], dividers.pluralize, keepZero);
+ if (humanized !== null) {
+ result.push(humanized);
+ }
+ });
+
+ if (opts.units) {
+ result = result.slice(0, opts.units);
+ }
+
+ result = result.join(joiner);
+
+ if (process) {
+ result = process(result);
+ }
+
+ return result.length === 0 ? null : result;
+}
diff --git a/app/assets/javascripts/ci_lint/index.js b/app/assets/javascripts/ci_lint/index.js
index f97590ec5db..274aab45deb 100644
--- a/app/assets/javascripts/ci_lint/index.js
+++ b/app/assets/javascripts/ci_lint/index.js
@@ -8,9 +8,7 @@ import CiLint from './components/ci_lint.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers, {
- assumeImmutableResults: true,
- }),
+ defaultClient: createDefaultClient(resolvers),
});
export default (containerId = '#js-ci-lint') => {
diff --git a/app/assets/javascripts/clusters/agents/components/show.vue b/app/assets/javascripts/clusters/agents/components/show.vue
index 5c672d288c5..afbba9d1f7c 100644
--- a/app/assets/javascripts/clusters/agents/components/show.vue
+++ b/app/assets/javascripts/clusters/agents/components/show.vue
@@ -128,6 +128,7 @@ export default {
</p>
<gl-tabs>
+ <slot name="ee-security-tab"></slot>
<gl-tab>
<template #title>
<span data-testid="cluster-agent-token-count">
diff --git a/app/assets/javascripts/clusters/agents/index.js b/app/assets/javascripts/clusters/agents/index.js
index bcb5b271203..426d8d83847 100644
--- a/app/assets/javascripts/clusters/agents/index.js
+++ b/app/assets/javascripts/clusters/agents/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import AgentShowPage from './components/show.vue';
+import AgentShowPage from 'ee_else_ce/clusters/agents/components/show.vue';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
index 0da7be4040f..98db620e3ab 100644
--- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -1,8 +1,7 @@
<script>
-import { GlModal, GlButton, GlFormInput } from '@gitlab/ui';
-import { escape } from 'lodash';
+import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
-import { s__, sprintf } from '~/locale';
+import { s__ } from '~/locale';
import SplitButton from '~/vue_shared/components/split_button.vue';
const splitButtonActionItems = [
@@ -29,6 +28,7 @@ export default {
GlModal,
GlButton,
GlFormInput,
+ GlSprintf,
},
props: {
clusterPath: {
@@ -67,17 +67,11 @@ export default {
: s__('ClusterIntegration|You are about to remove your cluster integration.');
},
confirmationTextLabel() {
- return sprintf(
- this.confirmCleanup
- ? s__(
- 'ClusterIntegration|To remove your integration and resources, type %{clusterName} to confirm:',
- )
- : s__('ClusterIntegration|To remove your integration, type %{clusterName} to confirm:'),
- {
- clusterName: `<code>${escape(this.clusterName)}</code>`,
- },
- false,
- );
+ return this.confirmCleanup
+ ? s__(
+ 'ClusterIntegration|To remove your integration and resources, type %{clusterName} to confirm:',
+ )
+ : s__('ClusterIntegration|To remove your integration, type %{clusterName} to confirm:');
},
canSubmit() {
return this.enteredClusterName === this.clusterName;
@@ -140,7 +134,13 @@ export default {
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
</ul>
</div>
- <strong v-html="confirmationTextLabel /* eslint-disable-line vue/no-v-html */"></strong>
+ <strong>
+ <gl-sprintf :message="confirmationTextLabel">
+ <template #clusterName>
+ <code>{{ clusterName }}</code>
+ </template>
+ </gl-sprintf>
+ </strong>
<form ref="form" :action="clusterPath" method="post" class="gl-mb-5">
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
@@ -159,7 +159,7 @@ export default {
)
}}</span>
<template #modal-footer>
- <gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button>
+ <gl-button variant="secondary" @click="handleCancel">{{ __('Cancel') }}</gl-button>
<template v-if="confirmCleanup">
<gl-button
:disabled="!canSubmit"
diff --git a/app/assets/javascripts/clusters_list/clusters_util.js b/app/assets/javascripts/clusters_list/clusters_util.js
index 9b870134512..c78c93fe1ba 100644
--- a/app/assets/javascripts/clusters_list/clusters_util.js
+++ b/app/assets/javascripts/clusters_list/clusters_util.js
@@ -6,3 +6,7 @@ export function generateAgentRegistrationCommand(agentToken, kasAddress) {
--agent-version stable \\
--namespace gitlab-kubernetes-agent | kubectl apply -f -`;
}
+
+export function getAgentConfigPath(clusterAgentName) {
+ return `.gitlab/agents/${clusterAgentName}`;
+}
diff --git a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
index 405339b3d36..af44a23b4b3 100644
--- a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
@@ -1,9 +1,16 @@
<script>
import { GlButton, GlEmptyState, GlLink, GlSprintf, GlAlert, GlModalDirective } from '@gitlab/ui';
-import { INSTALL_AGENT_MODAL_ID } from '../constants';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { INSTALL_AGENT_MODAL_ID, I18N_AGENTS_EMPTY_STATE } from '../constants';
export default {
+ i18n: I18N_AGENTS_EMPTY_STATE,
modalId: INSTALL_AGENT_MODAL_ID,
+ multipleClustersDocsUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
+ installDocsUrl: helpPagePath('administration/clusters/kas'),
+ getStartedDocsUrl: helpPagePath('user/clusters/agent/index', {
+ anchor: 'define-a-configuration-repository',
+ }),
components: {
GlButton,
GlEmptyState,
@@ -14,19 +21,17 @@ export default {
directives: {
GlModalDirective,
},
- inject: [
- 'emptyStateImage',
- 'projectPath',
- 'agentDocsUrl',
- 'installDocsUrl',
- 'getStartedDocsUrl',
- 'integrationDocsUrl',
- ],
+ inject: ['emptyStateImage', 'projectPath'],
props: {
hasConfigurations: {
type: Boolean,
required: true,
},
+ isChildComponent: {
+ default: false,
+ required: false,
+ type: Boolean,
+ },
},
computed: {
repositoryPath() {
@@ -37,22 +42,19 @@ export default {
</script>
<template>
- <gl-empty-state
- :svg-path="emptyStateImage"
- :title="s__('ClusterAgents|Integrate Kubernetes with a GitLab Agent')"
- class="empty-state--agent"
- >
+ <gl-empty-state :svg-path="emptyStateImage" title="" class="agents-empty-state">
<template #description>
- <p class="mw-460 gl-mx-auto">
- <gl-sprintf
- :message="
- s__(
- 'ClusterAgents|The GitLab Kubernetes Agent allows an Infrastructure as Code, GitOps approach to integrating Kubernetes clusters with GitLab. %{linkStart}Learn more.%{linkEnd}',
- )
- "
- >
+ <p class="mw-460 gl-mx-auto gl-text-left">
+ {{ $options.i18n.introText }}
+ </p>
+ <p class="mw-460 gl-mx-auto gl-text-left">
+ <gl-sprintf :message="$options.i18n.multipleClustersText">
<template #link="{ content }">
- <gl-link :href="agentDocsUrl" target="_blank" data-testid="agent-docs-link">
+ <gl-link
+ :href="$options.multipleClustersDocsUrl"
+ target="_blank"
+ data-testid="multiple-clusters-docs-link"
+ >
{{ content }}
</gl-link>
</template>
@@ -60,19 +62,9 @@ export default {
</p>
<p class="mw-460 gl-mx-auto">
- <gl-sprintf
- :message="
- s__(
- 'ClusterAgents|The GitLab Agent also requires %{linkStart}enabling the Agent Server%{linkEnd}',
- )
- "
- >
- <template #link="{ content }">
- <gl-link :href="installDocsUrl" target="_blank" data-testid="install-docs-link">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
+ <gl-link :href="$options.installDocsUrl" target="_blank" data-testid="install-docs-link">
+ {{ $options.i18n.learnMoreText }}
+ </gl-link>
</p>
<gl-alert
@@ -81,24 +73,20 @@ export default {
class="gl-mb-5 text-left"
:dismissible="false"
>
- {{
- s__(
- 'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.',
- )
- }}
+ {{ $options.i18n.warningText }}
<template #actions>
<gl-button
category="primary"
variant="info"
- :href="getStartedDocsUrl"
+ :href="$options.getStartedDocsUrl"
target="_blank"
class="gl-ml-0!"
>
- {{ s__('ClusterAgents|Read more about getting started') }}
+ {{ $options.i18n.readMoreText }}
</gl-button>
<gl-button category="secondary" variant="info" :href="repositoryPath">
- {{ s__('ClusterAgents|Go to the repository') }}
+ {{ $options.i18n.repositoryButtonText }}
</gl-button>
</template>
</gl-alert>
@@ -106,13 +94,14 @@ export default {
<template #actions>
<gl-button
+ v-if="!isChildComponent"
v-gl-modal-directive="$options.modalId"
:disabled="!hasConfigurations"
data-testid="integration-primary-button"
category="primary"
- variant="success"
+ variant="confirm"
>
- {{ s__('ClusterAgents|Integrate with the GitLab Agent') }}
+ {{ $options.i18n.primaryButtonText }}
</gl-button>
</template>
</gl-empty-state>
diff --git a/app/assets/javascripts/clusters_list/components/agent_table.vue b/app/assets/javascripts/clusters_list/components/agent_table.vue
index 487e512c06d..000730ac1ba 100644
--- a/app/assets/javascripts/clusters_list/components/agent_table.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_table.vue
@@ -1,6 +1,5 @@
<script>
import {
- GlButton,
GlLink,
GlModalDirective,
GlTable,
@@ -12,11 +11,12 @@ import {
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import { INSTALL_AGENT_MODAL_ID, AGENT_STATUSES, TROUBLESHOOTING_LINK } from '../constants';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { INSTALL_AGENT_MODAL_ID, AGENT_STATUSES } from '../constants';
+import { getAgentConfigPath } from '../clusters_util';
export default {
components: {
- GlButton,
GlLink,
GlTable,
GlIcon,
@@ -29,10 +29,12 @@ export default {
GlModalDirective,
},
mixins: [timeagoMixin],
- inject: ['integrationDocsUrl'],
INSTALL_AGENT_MODAL_ID,
AGENT_STATUSES,
- TROUBLESHOOTING_LINK,
+
+ troubleshooting_link: helpPagePath('user/clusters/agent/index', {
+ anchor: 'troubleshooting',
+ }),
props: {
agents: {
required: true,
@@ -41,112 +43,102 @@ export default {
},
computed: {
fields() {
+ const tdClass = 'gl-py-5!';
return [
{
key: 'name',
label: s__('ClusterAgents|Name'),
+ tdClass,
},
{
key: 'status',
label: s__('ClusterAgents|Connection status'),
+ tdClass,
},
{
key: 'lastContact',
label: s__('ClusterAgents|Last contact'),
+ tdClass,
},
{
key: 'configuration',
label: s__('ClusterAgents|Configuration'),
+ tdClass,
},
];
},
},
+ methods: {
+ getCellId(item) {
+ return `connection-status-${item.name}`;
+ },
+ getAgentConfigPath,
+ },
};
</script>
<template>
- <div>
- <div class="gl-display-block gl-text-right gl-my-3">
- <gl-button
- v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
- variant="confirm"
- category="primary"
- >{{ s__('ClusterAgents|Install a new GitLab Agent') }}
- </gl-button>
- </div>
+ <gl-table
+ :items="agents"
+ :fields="fields"
+ stacked="md"
+ head-variant="white"
+ thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
+ class="gl-mb-4!"
+ data-testid="cluster-agent-list-table"
+ >
+ <template #cell(name)="{ item }">
+ <gl-link :href="item.webPath" data-testid="cluster-agent-name-link">
+ {{ item.name }}
+ </gl-link>
+ </template>
- <gl-table
- :items="agents"
- :fields="fields"
- stacked="md"
- head-variant="white"
- thead-class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
- data-testid="cluster-agent-list-table"
- >
- <template #cell(name)="{ item }">
- <gl-link :href="item.webPath" data-testid="cluster-agent-name-link">
- {{ item.name }}
- </gl-link>
- </template>
-
- <template #cell(status)="{ item }">
- <span
- :id="`connection-status-${item.name}`"
- class="gl-pr-5"
- data-testid="cluster-agent-connection-status"
- >
- <span :class="$options.AGENT_STATUSES[item.status].class" class="gl-mr-3">
- <gl-icon :name="$options.AGENT_STATUSES[item.status].icon" :size="12" /></span
- >{{ $options.AGENT_STATUSES[item.status].name }}
- </span>
- <gl-tooltip
- v-if="item.status === 'active'"
- :target="`connection-status-${item.name}`"
- placement="right"
- >
- <gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.title"
- ><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template>
- </gl-sprintf>
- </gl-tooltip>
- <gl-popover
- v-else
- :target="`connection-status-${item.name}`"
- :title="$options.AGENT_STATUSES[item.status].tooltip.title"
- placement="right"
- container="viewport"
- >
- <p>
- <gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.body"
- ><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template></gl-sprintf
- >
- </p>
- <p class="gl-mb-0">
- {{ s__('ClusterAgents|For more troubleshooting information go to') }}
- <gl-link :href="$options.TROUBLESHOOTING_LINK" target="_blank" class="gl-font-sm">
- {{ $options.TROUBLESHOOTING_LINK }}</gl-link
- >
- </p>
- </gl-popover>
- </template>
+ <template #cell(status)="{ item }">
+ <span :id="getCellId(item)" class="gl-md-pr-5" data-testid="cluster-agent-connection-status">
+ <span :class="$options.AGENT_STATUSES[item.status].class" class="gl-mr-3">
+ <gl-icon :name="$options.AGENT_STATUSES[item.status].icon" :size="12" /></span
+ >{{ $options.AGENT_STATUSES[item.status].name }}
+ </span>
+ <gl-tooltip v-if="item.status === 'active'" :target="getCellId(item)" placement="right">
+ <gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.title"
+ ><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template>
+ </gl-sprintf>
+ </gl-tooltip>
+ <gl-popover
+ v-else
+ :target="getCellId(item)"
+ :title="$options.AGENT_STATUSES[item.status].tooltip.title"
+ placement="right"
+ container="viewport"
+ >
+ <p>
+ <gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.body"
+ ><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template></gl-sprintf
+ >
+ </p>
+ <p class="gl-mb-0">
+ <gl-link :href="$options.troubleshooting_link" target="_blank" class="gl-font-sm">
+ {{ s__('ClusterAgents|Learn how to troubleshoot') }}</gl-link
+ >
+ </p>
+ </gl-popover>
+ </template>
- <template #cell(lastContact)="{ item }">
- <span data-testid="cluster-agent-last-contact">
- <time-ago-tooltip v-if="item.lastContact" :time="item.lastContact" />
- <span v-else>{{ s__('ClusterAgents|Never') }}</span>
- </span>
- </template>
+ <template #cell(lastContact)="{ item }">
+ <span data-testid="cluster-agent-last-contact">
+ <time-ago-tooltip v-if="item.lastContact" :time="item.lastContact" />
+ <span v-else>{{ s__('ClusterAgents|Never') }}</span>
+ </span>
+ </template>
- <template #cell(configuration)="{ item }">
- <span data-testid="cluster-agent-configuration-link">
- <!-- eslint-disable @gitlab/vue-require-i18n-strings -->
- <gl-link v-if="item.configFolder" :href="item.configFolder.webPath">
- .gitlab/agents/{{ item.name }}
- </gl-link>
+ <template #cell(configuration)="{ item }">
+ <span data-testid="cluster-agent-configuration-link">
+ <gl-link v-if="item.configFolder" :href="item.configFolder.webPath">
+ {{ getAgentConfigPath(item.name) }}
+ </gl-link>
- <span v-else>.gitlab/agents/{{ item.name }}</span>
- <!-- eslint-enable @gitlab/vue-require-i18n-strings -->
- </span>
- </template>
- </gl-table>
- </div>
+ <span v-else>{{ getAgentConfigPath(item.name) }}</span>
+ </span>
+ </template>
+ </gl-table>
</template>
diff --git a/app/assets/javascripts/clusters_list/components/agents.vue b/app/assets/javascripts/clusters_list/components/agents.vue
index ed44c1f5fa7..fb5cf7d1206 100644
--- a/app/assets/javascripts/clusters_list/components/agents.vue
+++ b/app/assets/javascripts/clusters_list/components/agents.vue
@@ -4,7 +4,6 @@ import { MAX_LIST_COUNT, ACTIVE_CONNECTION_TIME } from '../constants';
import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import AgentEmptyState from './agent_empty_state.vue';
import AgentTable from './agent_table.vue';
-import InstallAgentModal from './install_agent_modal.vue';
export default {
apollo: {
@@ -21,12 +20,14 @@ export default {
this.updateTreeList(data);
return data;
},
+ result() {
+ this.emitAgentsLoaded();
+ },
},
},
components: {
AgentEmptyState,
AgentTable,
- InstallAgentModal,
GlAlert,
GlKeysetPagination,
GlLoadingIcon,
@@ -38,11 +39,21 @@ export default {
required: false,
type: String,
},
+ isChildComponent: {
+ default: false,
+ required: false,
+ type: Boolean,
+ },
+ limit: {
+ default: null,
+ required: false,
+ type: Number,
+ },
},
data() {
return {
cursor: {
- first: MAX_LIST_COUNT,
+ first: this.limit ? this.limit : MAX_LIST_COUNT,
last: null,
},
folderList: {},
@@ -70,7 +81,7 @@ export default {
return this.$apollo.queries.agents.loading;
},
showPagination() {
- return this.agentPageInfo.hasPreviousPage || this.agentPageInfo.hasNextPage;
+ return !this.limit && (this.agentPageInfo.hasPreviousPage || this.agentPageInfo.hasNextPage);
},
treePageInfo() {
return this.agents?.project?.repository?.tree?.trees?.pageInfo || {};
@@ -130,24 +141,31 @@ export default {
}
return 'unused';
},
+ emitAgentsLoaded() {
+ const count = this.agents?.project?.clusterAgents?.count;
+ this.$emit('onAgentsLoad', count);
+ },
},
};
</script>
<template>
- <gl-loading-icon v-if="isLoading" size="md" class="gl-mt-3" />
+ <gl-loading-icon v-if="isLoading" size="md" />
- <section v-else-if="agentList" class="gl-mt-3">
+ <section v-else-if="agentList">
<div v-if="agentList.length">
- <AgentTable :agents="agentList" />
+ <agent-table :agents="agentList" />
<div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-keyset-pagination v-bind="agentPageInfo" @prev="prevPage" @next="nextPage" />
</div>
</div>
- <AgentEmptyState v-else :has-configurations="hasConfigurations" />
- <InstallAgentModal @agentRegistered="reloadAgents" />
+ <agent-empty-state
+ v-else
+ :has-configurations="hasConfigurations"
+ :is-child-component="isChildComponent"
+ />
</section>
<gl-alert v-else variant="danger" :dismissible="false">
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 0d1534d20e0..9c330045596 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -14,6 +14,7 @@ import { __, sprintf } from '~/locale';
import { CLUSTER_TYPES, STATUSES } from '../constants';
import AncestorNotice from './ancestor_notice.vue';
import NodeErrorHelpText from './node_error_help_text.vue';
+import ClustersEmptyState from './clusters_empty_state.vue';
export default {
nodeMemoryText: __('%{totalMemory} (%{freeSpacePercentage}%{percentSymbol} free)'),
@@ -28,10 +29,23 @@ export default {
GlSprintf,
GlTable,
NodeErrorHelpText,
+ ClustersEmptyState,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ props: {
+ isChildComponent: {
+ default: false,
+ required: false,
+ type: Boolean,
+ },
+ limit: {
+ default: null,
+ required: false,
+ type: Number,
+ },
+ },
computed: {
...mapState([
'clusters',
@@ -40,7 +54,7 @@ export default {
'loadingNodes',
'page',
'providers',
- 'totalCulsters',
+ 'totalClusters',
]),
contentAlignClasses() {
return 'gl-display-flex gl-align-items-center gl-justify-content-end gl-justify-content-md-start';
@@ -55,43 +69,57 @@ export default {
},
},
fields() {
+ const tdClass = 'gl-py-5!';
return [
{
key: 'name',
label: __('Kubernetes cluster'),
+ tdClass,
},
{
key: 'environment_scope',
label: __('Environment scope'),
+ tdClass,
},
{
key: 'node_size',
label: __('Nodes'),
+ tdClass,
},
{
key: 'total_cpu',
label: __('Total cores (CPUs)'),
+ tdClass,
},
{
key: 'total_memory',
label: __('Total memory (GB)'),
+ tdClass,
},
{
key: 'cluster_type',
label: __('Cluster level'),
+ tdClass,
formatter: (value) => CLUSTER_TYPES[value],
},
];
},
- hasClusters() {
+ hasClustersPerPage() {
return this.clustersPerPage > 0;
},
+ hasClusters() {
+ return this.totalClusters > 0;
+ },
},
mounted() {
+ if (this.limit) {
+ this.setClustersPerPage(this.limit);
+ }
+
this.fetchClusters();
},
methods: {
- ...mapActions(['fetchClusters', 'reportSentryError', 'setPage']),
+ ...mapActions(['fetchClusters', 'reportSentryError', 'setPage', 'setClustersPerPage']),
k8sQuantityToGb(quantity) {
if (!quantity) {
return 0;
@@ -196,18 +224,20 @@ export default {
</script>
<template>
- <gl-loading-icon v-if="loadingClusters" size="md" class="gl-mt-3" />
+ <gl-loading-icon v-if="loadingClusters" size="md" />
<section v-else>
<ancestor-notice />
<gl-table
+ v-if="hasClusters"
:items="clusters"
:fields="fields"
+ fixed
stacked="md"
head-variant="white"
- thead-class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
- class="qa-clusters-table"
+ thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
+ class="qa-clusters-table gl-mb-4!"
data-testid="cluster_list_table"
>
<template #cell(name)="{ item }">
@@ -241,7 +271,7 @@ export default {
<gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
- <NodeErrorHelpText
+ <node-error-help-text
v-else-if="item.kubernetes_errors"
:class="contentAlignClasses"
:error-type="item.kubernetes_errors.connection_error"
@@ -262,7 +292,7 @@ export default {
<gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
- <NodeErrorHelpText
+ <node-error-help-text
v-else-if="item.kubernetes_errors"
:class="contentAlignClasses"
:error-type="item.kubernetes_errors.node_connection_error"
@@ -283,7 +313,7 @@ export default {
<gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
- <NodeErrorHelpText
+ <node-error-help-text
v-else-if="item.kubernetes_errors"
:class="contentAlignClasses"
:error-type="item.kubernetes_errors.metrics_connection_error"
@@ -298,11 +328,13 @@ export default {
</template>
</gl-table>
+ <clusters-empty-state v-else :is-child-component="isChildComponent" />
+
<gl-pagination
- v-if="hasClusters"
+ v-if="hasClustersPerPage && !limit"
v-model="currentPage"
:per-page="clustersPerPage"
- :total-items="totalCulsters"
+ :total-items="totalClusters"
:prev-text="__('Prev')"
:next-text="__('Next')"
align="center"
diff --git a/app/assets/javascripts/clusters_list/components/clusters_actions.vue b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
new file mode 100644
index 00000000000..25f67462223
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlModalDirective } from '@gitlab/ui';
+import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '../constants';
+
+export default {
+ i18n: CLUSTERS_ACTIONS,
+ INSTALL_AGENT_MODAL_ID,
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ inject: ['newClusterPath', 'addClusterPath'],
+};
+</script>
+
+<template>
+ <div class="nav-controls gl-ml-auto">
+ <gl-dropdown
+ ref="dropdown"
+ v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
+ category="primary"
+ variant="confirm"
+ :text="$options.i18n.actionsButton"
+ split
+ right
+ >
+ <gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
+ {{ $options.i18n.createNewCluster }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
+ data-testid="connect-new-agent-link"
+ >
+ {{ $options.i18n.connectWithAgent }}
+ </gl-dropdown-item>
+ <gl-dropdown-item :href="addClusterPath" data-testid="connect-cluster-link" @click.stop>
+ {{ $options.i18n.connectExistingCluster }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
new file mode 100644
index 00000000000..3879af6e9cb
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlEmptyState, GlButton, GlLink, GlSprintf } from '@gitlab/ui';
+import { mapState } from 'vuex';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { I18N_CLUSTERS_EMPTY_STATE } from '../constants';
+
+export default {
+ i18n: I18N_CLUSTERS_EMPTY_STATE,
+ components: {
+ GlEmptyState,
+ GlButton,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['emptyStateHelpText', 'clustersEmptyStateImage', 'newClusterPath'],
+ props: {
+ isChildComponent: {
+ default: false,
+ required: false,
+ type: Boolean,
+ },
+ },
+ learnMoreHelpUrl: helpPagePath('user/project/clusters/index'),
+ multipleClustersHelpUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
+ computed: {
+ ...mapState(['canAddCluster']),
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state :svg-path="clustersEmptyStateImage" title="">
+ <template #description>
+ <p class="gl-text-left">
+ {{ $options.i18n.description }}
+ </p>
+ <p class="gl-text-left">
+ <gl-sprintf :message="$options.i18n.multipleClustersText">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.multipleClustersHelpUrl"
+ target="_blank"
+ data-testid="multiple-clusters-docs-link"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <p v-if="emptyStateHelpText" data-testid="clusters-empty-state-text">
+ {{ emptyStateHelpText }}
+ </p>
+
+ <p>
+ <gl-link :href="$options.learnMoreHelpUrl" target="_blank" data-testid="clusters-docs-link">
+ {{ $options.i18n.learnMoreLinkText }}
+ </gl-link>
+ </p>
+ </template>
+
+ <template #actions>
+ <gl-button
+ v-if="!isChildComponent"
+ data-testid="integration-primary-button"
+ data-qa-selector="add_kubernetes_cluster_link"
+ category="primary"
+ variant="confirm"
+ :disabled="!canAddCluster"
+ :href="newClusterPath"
+ >
+ {{ $options.i18n.buttonText }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_main_view.vue b/app/assets/javascripts/clusters_list/components/clusters_main_view.vue
new file mode 100644
index 00000000000..9e03093aa67
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/clusters_main_view.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlTabs, GlTab } from '@gitlab/ui';
+import { CLUSTERS_TABS, MAX_CLUSTERS_LIST, MAX_LIST_COUNT, AGENT } from '../constants';
+import Agents from './agents.vue';
+import InstallAgentModal from './install_agent_modal.vue';
+import ClustersActions from './clusters_actions.vue';
+import Clusters from './clusters.vue';
+import ClustersViewAll from './clusters_view_all.vue';
+
+export default {
+ components: {
+ GlTabs,
+ GlTab,
+ ClustersActions,
+ ClustersViewAll,
+ Clusters,
+ Agents,
+ InstallAgentModal,
+ },
+ CLUSTERS_TABS,
+ props: {
+ defaultBranchName: {
+ default: '.noBranch',
+ required: false,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ selectedTabIndex: 0,
+ maxAgents: MAX_CLUSTERS_LIST,
+ };
+ },
+ methods: {
+ onTabChange(tabName) {
+ this.selectedTabIndex = CLUSTERS_TABS.findIndex((tab) => tab.queryParamValue === tabName);
+
+ this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-tabs
+ v-model="selectedTabIndex"
+ sync-active-tab-with-query-params
+ nav-class="gl-flex-grow-1 gl-align-items-center"
+ lazy
+ >
+ <gl-tab
+ v-for="(tab, idx) in $options.CLUSTERS_TABS"
+ :key="idx"
+ :title="tab.title"
+ :query-param-value="tab.queryParamValue"
+ class="gl-line-height-20 gl-mt-5"
+ >
+ <component
+ :is="tab.component"
+ :default-branch-name="defaultBranchName"
+ data-testid="clusters-tab-component"
+ @changeTab="onTabChange"
+ />
+ </gl-tab>
+
+ <template #tabs-end>
+ <clusters-actions />
+ </template>
+ </gl-tabs>
+
+ <install-agent-modal :default-branch-name="defaultBranchName" :max-agents="maxAgents" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_view_all.vue b/app/assets/javascripts/clusters_list/components/clusters_view_all.vue
new file mode 100644
index 00000000000..285876e57d8
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/clusters_view_all.vue
@@ -0,0 +1,218 @@
+<script>
+import {
+ GlCard,
+ GlSprintf,
+ GlPopover,
+ GlLink,
+ GlButton,
+ GlBadge,
+ GlLoadingIcon,
+ GlModalDirective,
+} from '@gitlab/ui';
+import { mapState } from 'vuex';
+import {
+ AGENT_CARD_INFO,
+ CERTIFICATE_BASED_CARD_INFO,
+ MAX_CLUSTERS_LIST,
+ INSTALL_AGENT_MODAL_ID,
+} from '../constants';
+import Clusters from './clusters.vue';
+import Agents from './agents.vue';
+
+export default {
+ components: {
+ GlCard,
+ GlSprintf,
+ GlPopover,
+ GlLink,
+ GlButton,
+ GlBadge,
+ GlLoadingIcon,
+ Clusters,
+ Agents,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ AGENT_CARD_INFO,
+ CERTIFICATE_BASED_CARD_INFO,
+ MAX_CLUSTERS_LIST,
+ INSTALL_AGENT_MODAL_ID,
+ inject: ['addClusterPath'],
+ props: {
+ defaultBranchName: {
+ default: '.noBranch',
+ required: false,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ loadingAgents: true,
+ totalAgents: null,
+ };
+ },
+ computed: {
+ ...mapState(['loadingClusters', 'totalClusters']),
+ isLoading() {
+ return this.loadingAgents || this.loadingClusters;
+ },
+ agentsCardTitle() {
+ let cardTitle;
+ if (this.totalAgents > 0) {
+ cardTitle = {
+ message: AGENT_CARD_INFO.title,
+ number: this.totalAgents < MAX_CLUSTERS_LIST ? this.totalAgents : MAX_CLUSTERS_LIST,
+ total: this.totalAgents,
+ };
+ } else {
+ cardTitle = {
+ message: AGENT_CARD_INFO.emptyTitle,
+ };
+ }
+
+ return cardTitle;
+ },
+ clustersCardTitle() {
+ let cardTitle;
+ if (this.totalClusters > 0) {
+ cardTitle = {
+ message: CERTIFICATE_BASED_CARD_INFO.title,
+ number: this.totalClusters < MAX_CLUSTERS_LIST ? this.totalClusters : MAX_CLUSTERS_LIST,
+ total: this.totalClusters,
+ };
+ } else {
+ cardTitle = {
+ message: CERTIFICATE_BASED_CARD_INFO.emptyTitle,
+ };
+ }
+
+ return cardTitle;
+ },
+ },
+ methods: {
+ cardFooterNumber(number) {
+ return number > MAX_CLUSTERS_LIST ? number : '';
+ },
+ onAgentsLoad(number) {
+ this.totalAgents = number;
+ this.loadingAgents = false;
+ },
+ changeTab($event, tab) {
+ $event.preventDefault();
+ this.$emit('changeTab', tab);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" size="md" />
+ <div v-show="!isLoading" data-testid="clusters-cards-container">
+ <gl-card
+ header-class="gl-bg-white gl-display-flex gl-align-items-center gl-justify-content-space-between gl-py-4"
+ body-class="gl-pb-0"
+ footer-class="gl-text-right"
+ >
+ <template #header>
+ <h3 data-testid="agent-card-title" class="gl-my-0 gl-font-weight-normal gl-font-size-h2">
+ <gl-sprintf :message="agentsCardTitle.message"
+ ><template #number>{{ agentsCardTitle.number }}</template>
+ <template #total>{{ agentsCardTitle.total }}</template>
+ </gl-sprintf>
+ </h3>
+
+ <gl-badge id="clusters-recommended-badge" size="md" variant="info">{{
+ $options.AGENT_CARD_INFO.tooltip.label
+ }}</gl-badge>
+
+ <gl-popover
+ target="clusters-recommended-badge"
+ container="viewport"
+ placement="bottom"
+ :title="$options.AGENT_CARD_INFO.tooltip.title"
+ >
+ <p class="gl-mb-0">
+ <gl-sprintf :message="$options.AGENT_CARD_INFO.tooltip.text">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.AGENT_CARD_INFO.tooltip.link"
+ target="_blank"
+ class="gl-font-sm"
+ >
+ {{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-popover>
+ </template>
+
+ <agents
+ :limit="$options.MAX_CLUSTERS_LIST"
+ :default-branch-name="defaultBranchName"
+ :is-child-component="true"
+ @onAgentsLoad="onAgentsLoad"
+ />
+
+ <template #footer>
+ <gl-link
+ v-if="totalAgents"
+ data-testid="agents-tab-footer-link"
+ :href="`?tab=${$options.AGENT_CARD_INFO.tabName}`"
+ @click="changeTab($event, $options.AGENT_CARD_INFO.tabName)"
+ ><gl-sprintf :message="$options.AGENT_CARD_INFO.footerText"
+ ><template #number>{{ cardFooterNumber(totalAgents) }}</template></gl-sprintf
+ ></gl-link
+ ><gl-button
+ v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
+ class="gl-ml-4"
+ category="secondary"
+ variant="confirm"
+ >{{ $options.AGENT_CARD_INFO.actionText }}</gl-button
+ >
+ </template>
+ </gl-card>
+
+ <gl-card
+ class="gl-mt-6"
+ header-class="gl-bg-white gl-display-flex gl-align-items-center gl-justify-content-space-between"
+ body-class="gl-pb-0"
+ footer-class="gl-text-right"
+ >
+ <template #header>
+ <h3
+ class="gl-my-1 gl-font-weight-normal gl-font-size-h2"
+ data-testid="clusters-card-title"
+ >
+ <gl-sprintf :message="clustersCardTitle.message"
+ ><template #number>{{ clustersCardTitle.number }}</template>
+ <template #total>{{ clustersCardTitle.total }}</template>
+ </gl-sprintf>
+ </h3>
+ </template>
+
+ <clusters :limit="$options.MAX_CLUSTERS_LIST" :is-child-component="true" />
+
+ <template #footer>
+ <gl-link
+ v-if="totalClusters"
+ data-testid="clusters-tab-footer-link"
+ :href="`?tab=${$options.CERTIFICATE_BASED_CARD_INFO.tabName}`"
+ @click="changeTab($event, $options.CERTIFICATE_BASED_CARD_INFO.tabName)"
+ ><gl-sprintf :message="$options.CERTIFICATE_BASED_CARD_INFO.footerText"
+ ><template #number>{{ cardFooterNumber(totalClusters) }}</template></gl-sprintf
+ ></gl-link
+ ><gl-button
+ category="secondary"
+ data-qa-selector="connect_existing_cluster_button"
+ variant="confirm"
+ class="gl-ml-4"
+ :href="addClusterPath"
+ >{{ $options.CERTIFICATE_BASED_CARD_INFO.actionText }}</gl-button
+ >
+ </template>
+ </gl-card>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters_list/components/install_agent_modal.vue b/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
index 5f192fe4d5a..6eb2e85ecea 100644
--- a/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
+++ b/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
@@ -13,8 +13,10 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { generateAgentRegistrationCommand } from '../clusters_util';
import { INSTALL_AGENT_MODAL_ID, I18N_INSTALL_AGENT_MODAL } from '../constants';
+import { addAgentToStore } from '../graphql/cache_update';
import createAgent from '../graphql/mutations/create_agent.mutation.graphql';
import createAgentToken from '../graphql/mutations/create_agent_token.mutation.graphql';
+import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import AvailableAgentsDropdown from './available_agents_dropdown.vue';
export default {
@@ -33,12 +35,24 @@ export default {
GlSprintf,
},
inject: ['projectPath', 'kasAddress'],
+ props: {
+ defaultBranchName: {
+ default: '.noBranch',
+ required: false,
+ type: String,
+ },
+ maxAgents: {
+ required: true,
+ type: Number,
+ },
+ },
data() {
return {
registering: false,
agentName: null,
agentToken: null,
error: null,
+ clusterAgent: null,
};
},
computed: {
@@ -55,27 +69,31 @@ export default {
return generateAgentRegistrationCommand(this.agentToken, this.kasAddress);
},
basicInstallPath() {
- return helpPagePath('user/clusters/agent/index', {
+ return helpPagePath('user/clusters/agent/install/index', {
anchor: 'install-the-agent-into-the-cluster',
});
},
advancedInstallPath() {
- return helpPagePath('user/clusters/agent/index', { anchor: 'advanced-installation' });
+ return helpPagePath('user/clusters/agent/install/index', { anchor: 'advanced-installation' });
+ },
+ getAgentsQueryVariables() {
+ return {
+ defaultBranchName: this.defaultBranchName,
+ first: this.maxAgents,
+ last: null,
+ projectPath: this.projectPath,
+ };
},
},
methods: {
setAgentName(name) {
this.agentName = name;
},
- cancelClicked() {
- this.$refs.modal.hide();
- },
- doneClicked() {
- this.$emit('agentRegistered');
+ closeModal() {
this.$refs.modal.hide();
},
resetModal() {
- this.registering = null;
+ this.registering = false;
this.agentName = null;
this.agentToken = null;
this.error = null;
@@ -90,6 +108,14 @@ export default {
projectPath: this.projectPath,
},
},
+ update: (store, { data: { createClusterAgent } }) => {
+ addAgentToStore(
+ store,
+ createClusterAgent,
+ getAgentsQuery,
+ this.getAgentsQueryVariables,
+ );
+ },
})
.then(({ data: { createClusterAgent } }) => createClusterAgent);
},
@@ -117,6 +143,8 @@ export default {
throw new Error(agentErrors[0]);
}
+ this.clusterAgent = clusterAgent;
+
const { errors: tokenErrors, secret } = await this.createAgentTokenMutation(
clusterAgent.id,
);
@@ -240,10 +268,10 @@ export default {
</template>
<template #modal-footer>
- <gl-button v-if="canCancel" @click="cancelClicked">{{ $options.i18n.cancel }} </gl-button>
+ <gl-button v-if="canCancel" @click="closeModal">{{ $options.i18n.cancel }} </gl-button>
- <gl-button v-if="registered" variant="confirm" category="primary" @click="doneClicked"
- >{{ $options.i18n.done }}
+ <gl-button v-if="registered" variant="confirm" category="primary" @click="closeModal"
+ >{{ $options.i18n.close }}
</gl-button>
<gl-button
@@ -252,7 +280,7 @@ export default {
variant="confirm"
category="primary"
@click="registerAgent"
- >{{ $options.i18n.next }}
+ >{{ $options.i18n.registerAgentButton }}
</gl-button>
</template>
</gl-modal>
diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js
index 0bade1fc281..9fefdf450c4 100644
--- a/app/assets/javascripts/clusters_list/constants.js
+++ b/app/assets/javascripts/clusters_list/constants.js
@@ -1,10 +1,9 @@
import { __, s__, sprintf } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
export const MAX_LIST_COUNT = 25;
export const INSTALL_AGENT_MODAL_ID = 'install-agent';
export const ACTIVE_CONNECTION_TIME = 480000;
-export const TROUBLESHOOTING_LINK =
- 'https://docs.gitlab.com/ee/user/clusters/agent/#troubleshooting';
export const CLUSTER_ERRORS = {
default: {
@@ -66,8 +65,8 @@ export const STATUSES = {
};
export const I18N_INSTALL_AGENT_MODAL = {
- next: __('Next'),
- done: __('Done'),
+ registerAgentButton: s__('ClusterAgents|Register Agent'),
+ close: __('Close'),
cancel: __('Cancel'),
modalTitle: s__('ClusterAgents|Install new Agent'),
@@ -91,7 +90,7 @@ export const I18N_INSTALL_AGENT_MODAL = {
),
basicInstallTitle: s__('ClusterAgents|Recommended installation method'),
- basicInstallBody: s__(
+ basicInstallBody: __(
`Open a CLI and connect to the cluster you want to install the Agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`,
),
@@ -100,7 +99,7 @@ export const I18N_INSTALL_AGENT_MODAL = {
'ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}.',
),
- registrationErrorTitle: s__('Failed to register Agent'),
+ registrationErrorTitle: __('Failed to register Agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
};
@@ -141,3 +140,86 @@ export const AGENT_STATUSES = {
},
},
};
+
+export const I18N_AGENTS_EMPTY_STATE = {
+ introText: s__(
+ 'ClusterAgents|Use GitLab Agents to more securely integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more.',
+ ),
+ multipleClustersText: s__(
+ 'ClusterAgents|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}',
+ ),
+ learnMoreText: s__('ClusterAgents|Learn more about the GitLab Kubernetes Agent.'),
+ warningText: s__(
+ 'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.',
+ ),
+ readMoreText: s__('ClusterAgents|Read more about getting started'),
+ repositoryButtonText: s__('ClusterAgents|Go to the repository'),
+ primaryButtonText: s__('ClusterAgents|Connect with a GitLab Agent'),
+};
+
+export const I18N_CLUSTERS_EMPTY_STATE = {
+ description: s__(
+ 'ClusterIntegration|Use certificates to integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more in an easy way.',
+ ),
+ multipleClustersText: s__(
+ 'ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}',
+ ),
+ learnMoreLinkText: s__('ClusterIntegration|Learn more about the GitLab managed clusters'),
+ buttonText: s__('ClusterIntegration|Connect with a certificate'),
+};
+
+export const AGENT_CARD_INFO = {
+ tabName: 'agent',
+ title: sprintf(s__('ClusterAgents|%{number} of %{total} Agent based integrations')),
+ emptyTitle: s__('ClusterAgents|No Agent based integrations'),
+ tooltip: {
+ label: s__('ClusterAgents|Recommended'),
+ title: s__('ClusterAgents|GitLab Agents'),
+ text: sprintf(
+ s__(
+ 'ClusterAgents|GitLab Agents provide an increased level of security when integrating with clusters. %{linkStart}Learn more about the GitLab Kubernetes Agent.%{linkEnd}',
+ ),
+ ),
+ link: helpPagePath('user/clusters/agent/index'),
+ },
+ actionText: s__('ClusterAgents|Install new Agent'),
+ footerText: sprintf(s__('ClusterAgents|View all %{number} Agent based integrations')),
+};
+
+export const CERTIFICATE_BASED_CARD_INFO = {
+ tabName: 'certificate_based',
+ title: sprintf(s__('ClusterAgents|%{number} of %{total} Certificate based integrations')),
+ emptyTitle: s__('ClusterAgents|No Certificate based integrations'),
+ actionText: s__('ClusterAgents|Connect existing cluster'),
+ footerText: sprintf(s__('ClusterAgents|View all %{number} Certificate based integrations')),
+};
+
+export const MAX_CLUSTERS_LIST = 6;
+
+export const CLUSTERS_TABS = [
+ {
+ title: s__('ClusterAgents|All'),
+ component: 'ClustersViewAll',
+ queryParamValue: 'all',
+ },
+ {
+ title: s__('ClusterAgents|Agent'),
+ component: 'agents',
+ queryParamValue: 'agent',
+ },
+ {
+ title: s__('ClusterAgents|Certificate based'),
+ component: 'clusters',
+ queryParamValue: 'certificate_based',
+ },
+];
+
+export const CLUSTERS_ACTIONS = {
+ actionsButton: s__('ClusterAgents|Actions'),
+ createNewCluster: s__('ClusterAgents|Create new cluster'),
+ connectWithAgent: s__('ClusterAgents|Connect with Agent'),
+ connectExistingCluster: s__('ClusterAgents|Connect with certificate'),
+};
+
+export const AGENT = 'agent';
+export const CERTIFICATE_BASED = 'certificate_based';
diff --git a/app/assets/javascripts/clusters_list/graphql/cache_update.js b/app/assets/javascripts/clusters_list/graphql/cache_update.js
new file mode 100644
index 00000000000..dd633820952
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/graphql/cache_update.js
@@ -0,0 +1,29 @@
+import produce from 'immer';
+import { getAgentConfigPath } from '../clusters_util';
+
+export function addAgentToStore(store, createClusterAgent, query, variables) {
+ const { clusterAgent } = createClusterAgent;
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ const configuration = {
+ name: clusterAgent.name,
+ path: getAgentConfigPath(clusterAgent.name),
+ webPath: clusterAgent.webPath,
+ __typename: 'TreeEntry',
+ };
+
+ draftData.project.clusterAgents.nodes.push(clusterAgent);
+ draftData.project.clusterAgents.count += 1;
+ draftData.project.repository.tree.trees.nodes.push(configuration);
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+}
diff --git a/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql b/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql
new file mode 100644
index 00000000000..9b40260471c
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql
@@ -0,0 +1,10 @@
+fragment ClusterAgentFragment on ClusterAgent {
+ id
+ name
+ webPath
+ tokens {
+ nodes {
+ lastUsedAt
+ }
+ }
+}
diff --git a/app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql b/app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql
index c29756159f5..996b388089b 100644
--- a/app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql
+++ b/app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql
@@ -1,7 +1,9 @@
+#import "../fragments/cluster_agent.fragment.graphql"
+
mutation createClusterAgent($input: CreateClusterAgentInput!) {
createClusterAgent(input: $input) {
clusterAgent {
- id
+ ...ClusterAgentFragment
}
errors
}
diff --git a/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql b/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql
index 61989e00d9e..47b25988877 100644
--- a/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql
+++ b/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql
@@ -1,4 +1,5 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "../fragments/cluster_agent.fragment.graphql"
query getAgents(
$defaultBranchName: String!
@@ -13,19 +14,14 @@ query getAgents(
project(fullPath: $projectPath) {
clusterAgents(first: $first, last: $last, before: $beforeAgent, after: $afterAgent) {
nodes {
- id
- name
- webPath
- tokens {
- nodes {
- lastUsedAt
- }
- }
+ ...ClusterAgentFragment
}
pageInfo {
...PageInfo
}
+
+ count
}
repository {
diff --git a/app/assets/javascripts/clusters_list/index.js b/app/assets/javascripts/clusters_list/index.js
index de18965abbd..7f1ef37814b 100644
--- a/app/assets/javascripts/clusters_list/index.js
+++ b/app/assets/javascripts/clusters_list/index.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import loadClusters from './load_clusters';
-import loadAgents from './load_agents';
+import loadMainView from './load_main_view';
Vue.use(VueApollo);
export default () => {
loadClusters(Vue);
- loadAgents(Vue, VueApollo);
+ loadMainView(Vue, VueApollo);
};
diff --git a/app/assets/javascripts/clusters_list/load_agents.js b/app/assets/javascripts/clusters_list/load_agents.js
deleted file mode 100644
index b77d386df20..00000000000
--- a/app/assets/javascripts/clusters_list/load_agents.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import createDefaultClient from '~/lib/graphql';
-import Agents from './components/agents.vue';
-
-export default (Vue, VueApollo) => {
- const el = document.querySelector('#js-cluster-agents-list');
-
- if (!el) {
- return null;
- }
-
- const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
-
- const {
- emptyStateImage,
- defaultBranchName,
- projectPath,
- agentDocsUrl,
- installDocsUrl,
- getStartedDocsUrl,
- integrationDocsUrl,
- kasAddress,
- } = el.dataset;
-
- return new Vue({
- el,
- apolloProvider: new VueApollo({ defaultClient }),
- provide: {
- emptyStateImage,
- projectPath,
- agentDocsUrl,
- installDocsUrl,
- getStartedDocsUrl,
- integrationDocsUrl,
- kasAddress,
- },
- render(createElement) {
- return createElement(Agents, {
- props: {
- defaultBranchName,
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/clusters_list/load_clusters.js b/app/assets/javascripts/clusters_list/load_clusters.js
index 01430230879..1bb3ea546b2 100644
--- a/app/assets/javascripts/clusters_list/load_clusters.js
+++ b/app/assets/javascripts/clusters_list/load_clusters.js
@@ -8,8 +8,15 @@ export default (Vue) => {
return null;
}
+ const { emptyStateHelpText, newClusterPath, clustersEmptyStateImage } = el.dataset;
+
return new Vue({
el,
+ provide: {
+ emptyStateHelpText,
+ newClusterPath,
+ clustersEmptyStateImage,
+ },
store: createStore(el.dataset),
render(createElement) {
return createElement(Clusters);
diff --git a/app/assets/javascripts/clusters_list/load_main_view.js b/app/assets/javascripts/clusters_list/load_main_view.js
new file mode 100644
index 00000000000..08c99b46e16
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/load_main_view.js
@@ -0,0 +1,50 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import ClustersMainView from './components/clusters_main_view.vue';
+import { createStore } from './store';
+
+Vue.use(VueApollo);
+
+export default () => {
+ const el = document.querySelector('.js-clusters-main-view');
+
+ if (!el) {
+ return null;
+ }
+
+ const defaultClient = createDefaultClient();
+
+ const {
+ emptyStateImage,
+ defaultBranchName,
+ projectPath,
+ kasAddress,
+ newClusterPath,
+ addClusterPath,
+ emptyStateHelpText,
+ clustersEmptyStateImage,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider: new VueApollo({ defaultClient }),
+ provide: {
+ emptyStateImage,
+ projectPath,
+ kasAddress,
+ newClusterPath,
+ addClusterPath,
+ emptyStateHelpText,
+ clustersEmptyStateImage,
+ },
+ store: createStore(el.dataset),
+ render(createElement) {
+ return createElement(ClustersMainView, {
+ props: {
+ defaultBranchName,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index 5f35a0b26f3..d70b36e63bc 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -3,7 +3,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
import { MAX_REQUESTS } from '../constants';
import * as types from './mutation_types';
@@ -30,7 +30,13 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
const poll = new Poll({
resource: {
- fetchClusters: (paginatedEndPoint) => axios.get(paginatedEndPoint),
+ fetchClusters: (paginatedEndPoint) =>
+ axios.get(paginatedEndPoint, {
+ params: {
+ page: state.page,
+ per_page: state.clustersPerPage,
+ },
+ }),
},
data: `${state.endpoint}?page=${state.page}`,
method: 'fetchClusters',
@@ -65,7 +71,7 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
commit(types.SET_LOADING_CLUSTERS, false);
commit(types.SET_LOADING_NODES, false);
createFlash({
- message: __('Clusters|An error occurred while loading clusters'),
+ message: s__('Clusters|An error occurred while loading clusters'),
});
dispatch('reportSentryError', { error: response, tag: 'fetchClustersErrorCallback' });
@@ -78,3 +84,7 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
export const setPage = ({ commit }, page) => {
commit(types.SET_PAGE, page);
};
+
+export const setClustersPerPage = ({ commit }, limit) => {
+ commit(types.SET_CLUSTERS_PER_PAGE, limit);
+};
diff --git a/app/assets/javascripts/clusters_list/store/mutation_types.js b/app/assets/javascripts/clusters_list/store/mutation_types.js
index beb4388c93e..e88d4c74761 100644
--- a/app/assets/javascripts/clusters_list/store/mutation_types.js
+++ b/app/assets/javascripts/clusters_list/store/mutation_types.js
@@ -2,3 +2,4 @@ export const SET_CLUSTERS_DATA = 'SET_CLUSTERS_DATA';
export const SET_LOADING_CLUSTERS = 'SET_LOADING_CLUSTERS';
export const SET_LOADING_NODES = 'SET_LOADING_NODES';
export const SET_PAGE = 'SET_PAGE';
+export const SET_CLUSTERS_PER_PAGE = 'SET_CLUSTERS_PER_PAGE';
diff --git a/app/assets/javascripts/clusters_list/store/mutations.js b/app/assets/javascripts/clusters_list/store/mutations.js
index 5b462928518..93156c9200f 100644
--- a/app/assets/javascripts/clusters_list/store/mutations.js
+++ b/app/assets/javascripts/clusters_list/store/mutations.js
@@ -12,10 +12,13 @@ export default {
clusters: data.clusters,
clustersPerPage: paginationInformation.perPage,
hasAncestorClusters: data.has_ancestor_clusters,
- totalCulsters: paginationInformation.total,
+ totalClusters: paginationInformation.total,
});
},
[types.SET_PAGE](state, value) {
state.page = Number(value) || 1;
},
+ [types.SET_CLUSTERS_PER_PAGE](state, value) {
+ state.clustersPerPage = Number(value) || 1;
+ },
};
diff --git a/app/assets/javascripts/clusters_list/store/state.js b/app/assets/javascripts/clusters_list/store/state.js
index 51fafd49479..763d7389d0f 100644
--- a/app/assets/javascripts/clusters_list/store/state.js
+++ b/app/assets/javascripts/clusters_list/store/state.js
@@ -1,9 +1,11 @@
+import { parseBoolean } from '~/lib/utils/common_utils';
+
export default (initialState = {}) => ({
ancestorHelperPath: initialState.ancestorHelpPath,
endpoint: initialState.endpoint,
hasAncestorClusters: false,
clusters: [],
- clustersPerPage: 0,
+ clustersPerPage: 20,
loadingClusters: true,
loadingNodes: true,
page: 1,
@@ -12,5 +14,6 @@ export default (initialState = {}) => ({
default: { path: initialState.imgTagsDefaultPath, text: initialState.imgTagsDefaultText },
gcp: { path: initialState.imgTagsGcpPath, text: initialState.imgTagsGcpText },
},
- totalCulsters: 0,
+ totalClusters: 0,
+ canAddCluster: parseBoolean(initialState.canAddCluster),
});
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 5b9e70e3c09..ad70d9be16f 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -3,14 +3,14 @@ import { Rails } from '~/lib/utils/rails_ujs';
import { rstrip } from './lib/utils/common_utils';
function openConfirmDangerModal($form, $modal, text) {
- const $input = $('.js-confirm-danger-input', $modal);
+ const $input = $('.js-legacy-confirm-danger-input', $modal);
$input.val('');
$('.js-confirm-text', $modal).text(text || '');
$modal.modal('show');
- const confirmTextMatch = $('.js-confirm-danger-match', $modal).text();
- const $submit = $('.js-confirm-danger-submit', $modal);
+ const confirmTextMatch = $('.js-legacy-confirm-danger-match', $modal).text();
+ const $submit = $('.js-legacy-confirm-danger-submit', $modal);
$submit.disable();
$input.focus();
@@ -25,7 +25,7 @@ function openConfirmDangerModal($form, $modal, text) {
});
// eslint-disable-next-line @gitlab/no-global-event-off
- $('.js-confirm-danger-submit', $modal)
+ $('.js-legacy-confirm-danger-submit', $modal)
.off('click')
.on('click', () => {
if ($form.data('remote')) {
@@ -47,7 +47,7 @@ function getModal($btn) {
}
export default function initConfirmDangerModal() {
- $(document).on('click', '.js-confirm-danger', (e) => {
+ $(document).on('click', '.js-legacy-confirm-danger', (e) => {
const $btn = $(e.target);
const checkFieldName = $btn.data('checkFieldName');
const checkFieldCompareValue = $btn.data('checkCompareValue');
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 02ab34447ca..a8405fe37c7 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -3,7 +3,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
import { createContentEditor } from '../services/create_content_editor';
-import ContentEditorError from './content_editor_error.vue';
+import ContentEditorAlert from './content_editor_alert.vue';
import ContentEditorProvider from './content_editor_provider.vue';
import EditorStateObserver from './editor_state_observer.vue';
import FormattingBubbleMenu from './formatting_bubble_menu.vue';
@@ -12,7 +12,7 @@ import TopToolbar from './top_toolbar.vue';
export default {
components: {
GlLoadingIcon,
- ContentEditorError,
+ ContentEditorAlert,
ContentEditorProvider,
TiptapEditorContent,
TopToolbar,
@@ -92,7 +92,7 @@ export default {
<content-editor-provider :content-editor="contentEditor">
<div>
<editor-state-observer @docUpdate="notifyChange" @focus="focus" @blur="blur" />
- <content-editor-error />
+ <content-editor-alert />
<div
data-testid="content-editor"
data-qa-selector="content_editor_container"
diff --git a/app/assets/javascripts/content_editor/components/content_editor_alert.vue b/app/assets/javascripts/content_editor/components/content_editor_alert.vue
new file mode 100644
index 00000000000..c6737da1d77
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/content_editor_alert.vue
@@ -0,0 +1,33 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import EditorStateObserver from './editor_state_observer.vue';
+
+export default {
+ components: {
+ GlAlert,
+ EditorStateObserver,
+ },
+ data() {
+ return {
+ message: null,
+ variant: 'danger',
+ };
+ },
+ methods: {
+ displayAlert({ message, variant }) {
+ this.message = message;
+ this.variant = variant;
+ },
+ dismissAlert() {
+ this.message = null;
+ },
+ },
+};
+</script>
+<template>
+ <editor-state-observer @alert="displayAlert">
+ <gl-alert v-if="message" class="gl-mb-6" :variant="variant" @dismiss="dismissAlert">
+ {{ message }}
+ </gl-alert>
+ </editor-state-observer>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/content_editor_error.vue b/app/assets/javascripts/content_editor/components/content_editor_error.vue
deleted file mode 100644
index 031ea92a7e9..00000000000
--- a/app/assets/javascripts/content_editor/components/content_editor_error.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<script>
-import { GlAlert } from '@gitlab/ui';
-import EditorStateObserver from './editor_state_observer.vue';
-
-export default {
- components: {
- GlAlert,
- EditorStateObserver,
- },
- data() {
- return {
- error: null,
- };
- },
- methods: {
- displayError({ error }) {
- this.error = error;
- },
- dismissError() {
- this.error = null;
- },
- },
-};
-</script>
-<template>
- <editor-state-observer @error="displayError">
- <gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="dismissError">
- {{ error }}
- </gl-alert>
- </editor-state-observer>
-</template>
diff --git a/app/assets/javascripts/content_editor/components/editor_state_observer.vue b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
index 2eeb0719096..0604047a953 100644
--- a/app/assets/javascripts/content_editor/components/editor_state_observer.vue
+++ b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
@@ -7,7 +7,7 @@ export const tiptapToComponentMap = {
transaction: 'transaction',
focus: 'focus',
blur: 'blur',
- error: 'error',
+ alert: 'alert',
};
const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEventName];
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
index c44e8145982..41c083111c5 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
@@ -26,8 +26,8 @@ export default {
type: Object,
required: true,
},
- getPos: {
- type: Function,
+ node: {
+ type: Object,
required: true,
},
},
@@ -61,7 +61,17 @@ export default {
const { state } = this.editor;
const { $cursor } = state.selection;
- this.displayActionsDropdown = $cursor?.pos - $cursor?.parentOffset - 1 === this.getPos();
+ if (!$cursor) return;
+
+ this.displayActionsDropdown = false;
+
+ for (let level = 0; level < $cursor.depth; level += 1) {
+ if ($cursor.node(level) === this.node) {
+ this.displayActionsDropdown = true;
+ break;
+ }
+ }
+
if (this.displayActionsDropdown) {
this.selectedRect = getSelectedRect(state);
}
@@ -99,7 +109,11 @@ export default {
:as="cellType"
@click="hideDropdown"
>
- <span v-if="displayActionsDropdown" class="gl-absolute gl-right-0 gl-top-0">
+ <span
+ v-if="displayActionsDropdown"
+ contenteditable="false"
+ class="gl-absolute gl-right-0 gl-top-0"
+ >
<gl-dropdown
ref="dropdown"
dropup
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue
index 6b4343dd5b8..47cd837d060 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue
@@ -11,8 +11,8 @@ export default {
type: Object,
required: true,
},
- getPos: {
- type: Function,
+ node: {
+ type: Object,
required: true,
},
},
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue
index 5f9889374f6..150f78bc84f 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue
@@ -11,8 +11,8 @@ export default {
type: Object,
required: true,
},
- getPos: {
- type: Function,
+ node: {
+ type: Object,
required: true,
},
},
diff --git a/app/assets/javascripts/content_editor/extensions/blockquote.js b/app/assets/javascripts/content_editor/extensions/blockquote.js
index 4512ead44bc..5632bc28592 100644
--- a/app/assets/javascripts/content_editor/extensions/blockquote.js
+++ b/app/assets/javascripts/content_editor/extensions/blockquote.js
@@ -1,10 +1,8 @@
import { Blockquote } from '@tiptap/extension-blockquote';
-import { wrappingInputRule } from 'prosemirror-inputrules';
+import { wrappingInputRule } from '@tiptap/core';
import { getParents } from '~/lib/utils/dom_utils';
import { getMarkdownSource } from '../services/markdown_sourcemap';
-export const multilineInputRegex = /^\s*>>>\s$/gm;
-
export default Blockquote.extend({
addAttributes() {
return {
@@ -25,9 +23,15 @@ export default Blockquote.extend({
},
addInputRules() {
+ const multilineInputRegex = /^\s*>>>\s$/gm;
+
return [
...this.parent?.(),
- wrappingInputRule(multilineInputRegex, this.type, () => ({ multiline: true })),
+ wrappingInputRule({
+ find: multilineInputRegex,
+ type: this.type,
+ getAttributes: () => ({ multiline: true }),
+ }),
];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/description_list.js b/app/assets/javascripts/content_editor/extensions/description_list.js
index a516dfad2b8..8f5b145cfa3 100644
--- a/app/assets/javascripts/content_editor/extensions/description_list.js
+++ b/app/assets/javascripts/content_editor/extensions/description_list.js
@@ -1,7 +1,4 @@
-import { Node, mergeAttributes } from '@tiptap/core';
-import { wrappingInputRule } from 'prosemirror-inputrules';
-
-export const inputRegex = /^\s*(<dl>)$/;
+import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
export default Node.create({
name: 'descriptionList',
@@ -18,6 +15,8 @@ export default Node.create({
},
addInputRules() {
- return [wrappingInputRule(inputRegex, this.type)];
+ const inputRegex = /^\s*(<dl>)$/;
+
+ return [wrappingInputRule({ find: inputRegex, type: this.type })];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/details.js b/app/assets/javascripts/content_editor/extensions/details.js
index e3d54ed01fd..46c906d45b1 100644
--- a/app/assets/javascripts/content_editor/extensions/details.js
+++ b/app/assets/javascripts/content_editor/extensions/details.js
@@ -1,10 +1,7 @@
-import { Node } from '@tiptap/core';
+import { Node, wrappingInputRule } from '@tiptap/core';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
-import { wrappingInputRule } from 'prosemirror-inputrules';
import DetailsWrapper from '../components/wrappers/details.vue';
-export const inputRegex = /^\s*(<details>)$/;
-
export default Node.create({
name: 'details',
content: 'detailsContent+',
@@ -24,7 +21,9 @@ export default Node.create({
},
addInputRules() {
- return [wrappingInputRule(inputRegex, this.type)];
+ const inputRegex = /^\s*(<details>)$/;
+
+ return [wrappingInputRule({ find: inputRegex, type: this.type })];
},
addCommands() {
diff --git a/app/assets/javascripts/content_editor/extensions/emoji.js b/app/assets/javascripts/content_editor/extensions/emoji.js
index de608c3aaa2..7f8b5da5f46 100644
--- a/app/assets/javascripts/content_editor/extensions/emoji.js
+++ b/app/assets/javascripts/content_editor/extensions/emoji.js
@@ -1,9 +1,6 @@
-import { Node } from '@tiptap/core';
-import { InputRule } from 'prosemirror-inputrules';
+import { Node, InputRule } from '@tiptap/core';
import { initEmojiMap, getAllEmoji } from '~/emoji';
-export const emojiInputRegex = /(?:^|\s)((?::)((?:\w+))(?::))$/;
-
export default Node.create({
name: 'emoji',
@@ -54,23 +51,28 @@ export default Node.create({
},
addInputRules() {
+ const emojiInputRegex = /(?:^|\s)(:(\w+):)$/;
+
return [
- new InputRule(emojiInputRegex, (state, match, start, end) => {
- const [, , name] = match;
- const emojis = getAllEmoji();
- const emoji = emojis[name];
- const { tr } = state;
+ new InputRule({
+ find: emojiInputRegex,
+ handler: ({ state, range: { from, to }, match }) => {
+ const [, , name] = match;
+ const emojis = getAllEmoji();
+ const emoji = emojis[name];
+ const { tr } = state;
- if (emoji) {
- tr.replaceWith(start, end, [
- state.schema.text(' '),
- this.type.create({ name, moji: emoji.e, unicodeVersion: emoji.u, title: emoji.d }),
- ]);
+ if (emoji) {
+ tr.replaceWith(from, to, [
+ state.schema.text(' '),
+ this.type.create({ name, moji: emoji.e, unicodeVersion: emoji.u, title: emoji.d }),
+ ]);
- return tr;
- }
+ return tr;
+ }
- return null;
+ return null;
+ },
}),
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/frontmatter.js b/app/assets/javascripts/content_editor/extensions/frontmatter.js
index 64c84fe046b..c09c10bc524 100644
--- a/app/assets/javascripts/content_editor/extensions/frontmatter.js
+++ b/app/assets/javascripts/content_editor/extensions/frontmatter.js
@@ -17,4 +17,7 @@ export default CodeBlockHighlight.extend({
addNodeView() {
return new VueNodeViewRenderer(FrontmatterWrapper);
},
+ addInputRules() {
+ return [];
+ },
});
diff --git a/app/assets/javascripts/content_editor/extensions/horizontal_rule.js b/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
index c8ec45d835c..c4f31e5f981 100644
--- a/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
+++ b/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
@@ -1,10 +1,10 @@
import { nodeInputRule } from '@tiptap/core';
import { HorizontalRule } from '@tiptap/extension-horizontal-rule';
-export const hrInputRuleRegExp = /^---$/;
-
export default HorizontalRule.extend({
addInputRules() {
- return [nodeInputRule(hrInputRuleRegExp, this.type)];
+ const hrInputRuleRegExp = /^---$/;
+
+ return [nodeInputRule({ find: hrInputRuleRegExp, type: this.type })];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/html_marks.js b/app/assets/javascripts/content_editor/extensions/html_marks.js
index 54adb9efa0c..3abf0e3eee2 100644
--- a/app/assets/javascripts/content_editor/extensions/html_marks.js
+++ b/app/assets/javascripts/content_editor/extensions/html_marks.js
@@ -60,7 +60,13 @@ export default marks.map((name) =>
},
addInputRules() {
- return [markInputRule(markInputRegex(name), this.type, extractMarkAttributesFromMatch)];
+ return [
+ markInputRule({
+ find: markInputRegex(name),
+ type: this.type,
+ getAttributes: extractMarkAttributesFromMatch,
+ }),
+ ];
},
}),
);
diff --git a/app/assets/javascripts/content_editor/extensions/inline_diff.js b/app/assets/javascripts/content_editor/extensions/inline_diff.js
index 3bd328958df..22bb1ac072e 100644
--- a/app/assets/javascripts/content_editor/extensions/inline_diff.js
+++ b/app/assets/javascripts/content_editor/extensions/inline_diff.js
@@ -1,8 +1,5 @@
import { Mark, markInputRule, mergeAttributes } from '@tiptap/core';
-export const inputRegexAddition = /(\{\+(.+?)\+\})$/gm;
-export const inputRegexDeletion = /(\{-(.+?)-\})$/gm;
-
export default Mark.create({
name: 'inlineDiff',
@@ -38,9 +35,20 @@ export default Mark.create({
},
addInputRules() {
+ const inputRegexAddition = /(\{\+(.+?)\+\})$/gm;
+ const inputRegexDeletion = /(\{-(.+?)-\})$/gm;
+
return [
- markInputRule(inputRegexAddition, this.type, () => ({ type: 'addition' })),
- markInputRule(inputRegexDeletion, this.type, () => ({ type: 'deletion' })),
+ markInputRule({
+ find: inputRegexAddition,
+ type: this.type,
+ getAttributes: () => ({ type: 'addition' }),
+ }),
+ markInputRule({
+ find: inputRegexDeletion,
+ type: this.type,
+ getAttributes: () => ({ type: 'deletion' }),
+ }),
];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js
index fc0f38e6935..27bc05dce6f 100644
--- a/app/assets/javascripts/content_editor/extensions/link.js
+++ b/app/assets/javascripts/content_editor/extensions/link.js
@@ -1,9 +1,6 @@
import { markInputRule } from '@tiptap/core';
import { Link } from '@tiptap/extension-link';
-export const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm;
-export const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim;
-
const extractHrefFromMatch = (match) => {
return { href: match.groups.href };
};
@@ -26,9 +23,20 @@ export default Link.extend({
openOnClick: false,
},
addInputRules() {
+ const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm;
+ const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim;
+
return [
- markInputRule(markdownLinkSyntaxInputRuleRegExp, this.type, extractHrefFromMarkdownLink),
- markInputRule(urlSyntaxRegExp, this.type, extractHrefFromMatch),
+ markInputRule({
+ find: markdownLinkSyntaxInputRuleRegExp,
+ type: this.type,
+ getAttributes: extractHrefFromMarkdownLink,
+ }),
+ markInputRule({
+ find: urlSyntaxRegExp,
+ type: this.type,
+ getAttributes: extractHrefFromMatch,
+ }),
];
},
addAttributes() {
diff --git a/app/assets/javascripts/content_editor/extensions/math_inline.js b/app/assets/javascripts/content_editor/extensions/math_inline.js
index 60f5288dcf6..4844f6feb29 100644
--- a/app/assets/javascripts/content_editor/extensions/math_inline.js
+++ b/app/assets/javascripts/content_editor/extensions/math_inline.js
@@ -2,8 +2,6 @@ import { Mark, markInputRule } from '@tiptap/core';
import { __ } from '~/locale';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
-export const inputRegex = /(?:^|\s)\$`([^`]+)`\$$/gm;
-
export default Mark.create({
name: 'mathInline',
@@ -30,6 +28,8 @@ export default Mark.create({
},
addInputRules() {
- return [markInputRule(inputRegex, this.type)];
+ const inputRegex = /(?:^|\s)\$`([^`]+)`\$$/gm;
+
+ return [markInputRule({ find: inputRegex, type: this.type })];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/subscript.js b/app/assets/javascripts/content_editor/extensions/subscript.js
index d0766f42308..a8c087e8bf0 100644
--- a/app/assets/javascripts/content_editor/extensions/subscript.js
+++ b/app/assets/javascripts/content_editor/extensions/subscript.js
@@ -4,6 +4,12 @@ import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark
export default Subscript.extend({
addInputRules() {
- return [markInputRule(markInputRegex('sub'), this.type, extractMarkAttributesFromMatch)];
+ return [
+ markInputRule({
+ find: markInputRegex('sub'),
+ type: this.type,
+ getAttributes: extractMarkAttributesFromMatch,
+ }),
+ ];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/superscript.js b/app/assets/javascripts/content_editor/extensions/superscript.js
index 6cd814977ea..b86906f01f2 100644
--- a/app/assets/javascripts/content_editor/extensions/superscript.js
+++ b/app/assets/javascripts/content_editor/extensions/superscript.js
@@ -4,6 +4,12 @@ import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark
export default Superscript.extend({
addInputRules() {
- return [markInputRule(markInputRegex('sup'), this.type, extractMarkAttributesFromMatch)];
+ return [
+ markInputRule({
+ find: markInputRegex('sup'),
+ type: this.type,
+ getAttributes: extractMarkAttributesFromMatch,
+ }),
+ ];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/table.js b/app/assets/javascripts/content_editor/extensions/table.js
index 0f0477cba2e..004bb8b815c 100644
--- a/app/assets/javascripts/content_editor/extensions/table.js
+++ b/app/assets/javascripts/content_editor/extensions/table.js
@@ -1 +1,42 @@
-export { Table as default } from '@tiptap/extension-table';
+import { Table } from '@tiptap/extension-table';
+import { debounce } from 'lodash';
+import { __ } from '~/locale';
+import { getMarkdownSource } from '../services/markdown_sourcemap';
+import { shouldRenderHTMLTable } from '../services/serialization_helpers';
+
+let alertShown = false;
+const onUpdate = debounce((editor) => {
+ if (alertShown) return;
+
+ editor.state.doc.descendants((node) => {
+ if (node.type.name === 'table' && node.attrs.isMarkdown && shouldRenderHTMLTable(node)) {
+ editor.emit('alert', {
+ message: __(
+ 'The content editor may change the markdown formatting style of the document, which may not match your original markdown style.',
+ ),
+ variant: 'warning',
+ });
+
+ alertShown = true;
+
+ return false;
+ }
+
+ return true;
+ });
+}, 1000);
+
+export default Table.extend({
+ addAttributes() {
+ return {
+ isMarkdown: {
+ default: null,
+ parseHTML: (element) => Boolean(getMarkdownSource(element)),
+ },
+ };
+ },
+
+ onUpdate({ editor }) {
+ onUpdate(editor);
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/table_cell.js b/app/assets/javascripts/content_editor/extensions/table_cell.js
index befc33e669f..9f437ce066c 100644
--- a/app/assets/javascripts/content_editor/extensions/table_cell.js
+++ b/app/assets/javascripts/content_editor/extensions/table_cell.js
@@ -1,10 +1,9 @@
import { TableCell } from '@tiptap/extension-table-cell';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
import TableCellBodyWrapper from '../components/wrappers/table_cell_body.vue';
-import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableCell.extend({
- content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
+ content: 'block+',
addNodeView() {
return VueNodeViewRenderer(TableCellBodyWrapper);
diff --git a/app/assets/javascripts/content_editor/extensions/table_header.js b/app/assets/javascripts/content_editor/extensions/table_header.js
index 829b06fc14b..045fd03199b 100644
--- a/app/assets/javascripts/content_editor/extensions/table_header.js
+++ b/app/assets/javascripts/content_editor/extensions/table_header.js
@@ -1,10 +1,9 @@
import { TableHeader } from '@tiptap/extension-table-header';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
import TableCellHeaderWrapper from '../components/wrappers/table_cell_header.vue';
-import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableHeader.extend({
- content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
+ content: 'block+',
addNodeView() {
return VueNodeViewRenderer(TableCellHeaderWrapper);
},
diff --git a/app/assets/javascripts/content_editor/extensions/table_of_contents.js b/app/assets/javascripts/content_editor/extensions/table_of_contents.js
index 9e31158837e..a8882f9ede4 100644
--- a/app/assets/javascripts/content_editor/extensions/table_of_contents.js
+++ b/app/assets/javascripts/content_editor/extensions/table_of_contents.js
@@ -1,10 +1,7 @@
-import { Node } from '@tiptap/core';
-import { InputRule } from 'prosemirror-inputrules';
+import { Node, InputRule } from '@tiptap/core';
import { s__ } from '~/locale';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
-export const inputRuleRegExps = [/^\[\[_TOC_\]\]$/, /^\[TOC\]$/];
-
export default Node.create({
name: 'tableOfContents',
@@ -34,17 +31,21 @@ export default Node.create({
addInputRules() {
const { type } = this;
+ const inputRuleRegExps = [/^\[\[_TOC_\]\]$/, /^\[TOC\]$/];
return inputRuleRegExps.map(
(regex) =>
- new InputRule(regex, (state, match, start, end) => {
- const { tr } = state;
+ new InputRule({
+ find: regex,
+ handler: ({ state, range: { from, to }, match }) => {
+ const { tr } = state;
- if (match) {
- tr.replaceWith(start - 1, end, type.create());
- }
+ if (match) {
+ tr.replaceWith(from - 1, to, type.create());
+ }
- return tr;
+ return tr;
+ },
}),
);
},
diff --git a/app/assets/javascripts/content_editor/extensions/word_break.js b/app/assets/javascripts/content_editor/extensions/word_break.js
index 93b42466850..fa7e02f8cc8 100644
--- a/app/assets/javascripts/content_editor/extensions/word_break.js
+++ b/app/assets/javascripts/content_editor/extensions/word_break.js
@@ -1,7 +1,5 @@
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
-export const inputRegex = /^<wbr>$/;
-
export default Node.create({
name: 'wordBreak',
inline: true,
@@ -24,6 +22,8 @@ export default Node.create({
},
addInputRules() {
- return [nodeInputRule(inputRegex, this.type)];
+ const inputRegex = /^<wbr>$/;
+
+ return [nodeInputRule({ find: inputRegex, type: this.type })];
},
});
diff --git a/app/assets/javascripts/content_editor/services/feature_flags.js b/app/assets/javascripts/content_editor/services/feature_flags.js
deleted file mode 100644
index 5f7a4595938..00000000000
--- a/app/assets/javascripts/content_editor/services/feature_flags.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export function isBlockTablesFeatureEnabled() {
- return gon.features?.contentEditorBlockTables;
-}
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index b2327555b45..ed5910fca18 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -1,5 +1,4 @@
import { uniq } from 'lodash';
-import { isBlockTablesFeatureEnabled } from './feature_flags';
const defaultAttrs = {
td: { colspan: 1, rowspan: 1, colwidth: null },
@@ -75,7 +74,7 @@ function getChildren(node) {
return children;
}
-function shouldRenderHTMLTable(table) {
+export function shouldRenderHTMLTable(table) {
const { rows, cells } = getRowsAndCells(table);
const cellChildCount = Math.max(...cells.map((cell) => cell.childCount));
@@ -282,11 +281,6 @@ export function renderOrderedList(state, node) {
}
export function renderTableCell(state, node) {
- if (!isBlockTablesFeatureEnabled()) {
- state.renderInline(node);
- return;
- }
-
if (!isInBlockTable(node) || containsParagraphWithOnlyText(node)) {
state.renderInline(node.child(0));
} else {
@@ -303,9 +297,7 @@ export function renderTableRow(state, node) {
}
export function renderTable(state, node) {
- if (isBlockTablesFeatureEnabled()) {
- setIsInBlockTable(node, shouldRenderHTMLTable(node));
- }
+ setIsInBlockTable(node, shouldRenderHTMLTable(node));
if (isInBlockTable(node)) renderTagOpen(state, 'table');
@@ -317,9 +309,7 @@ export function renderTable(state, node) {
state.closeBlock(node);
state.flushClose();
- if (isBlockTablesFeatureEnabled()) {
- unsetIsInBlockTable(node);
- }
+ unsetIsInBlockTable(node);
}
export function renderHardBreak(state, node, parent, index) {
diff --git a/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js b/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
index d26f32a7e7a..9b1cb76f845 100644
--- a/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
+++ b/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
@@ -1,5 +1,5 @@
import { mapValues } from 'lodash';
-import { InputRule } from 'prosemirror-inputrules';
+import { InputRule } from '@tiptap/core';
import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys';
import Tracking from '~/tracking';
import {
@@ -17,17 +17,20 @@ const trackKeyboardShortcut = (contentType, commandFn, shortcut) => () => {
};
const trackInputRule = (contentType, inputRule) => {
- return new InputRule(inputRule.match, (...args) => {
- const result = inputRule.handler(...args);
+ return new InputRule({
+ find: inputRule.find,
+ handler: (...args) => {
+ const result = inputRule.handler(...args);
- if (result) {
- Tracking.event(undefined, INPUT_RULE_TRACKING_ACTION, {
- label: CONTENT_EDITOR_TRACKING_LABEL,
- property: contentType,
- });
- }
+ if (result !== null) {
+ Tracking.event(undefined, INPUT_RULE_TRACKING_ACTION, {
+ label: CONTENT_EDITOR_TRACKING_LABEL,
+ property: contentType,
+ });
+ }
- return result;
+ return result;
+ },
});
};
diff --git a/app/assets/javascripts/content_editor/services/upload_helpers.js b/app/assets/javascripts/content_editor/services/upload_helpers.js
index 8ac3f719309..f5bf2742748 100644
--- a/app/assets/javascripts/content_editor/services/upload_helpers.js
+++ b/app/assets/javascripts/content_editor/services/upload_helpers.js
@@ -72,8 +72,9 @@ const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => {
);
} catch (e) {
editor.commands.deleteRange({ from: position, to: position + 1 });
- editor.emit('error', {
- error: __('An error occurred while uploading the image. Please try again.'),
+ editor.emit('alert', {
+ message: __('An error occurred while uploading the image. Please try again.'),
+ variant: 'danger',
});
}
};
@@ -102,8 +103,9 @@ const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) =
);
} catch (e) {
editor.commands.deleteRange({ from, to: from + 1 });
- editor.emit('error', {
- error: __('An error occurred while uploading the file. Please try again.'),
+ editor.emit('alert', {
+ message: __('An error occurred while uploading the file. Please try again.'),
+ variant: 'danger',
});
}
};
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index 08cf0197993..08942374120 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -2,6 +2,8 @@ import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Cookies from 'js-cookie';
import { debounce } from 'lodash';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { parseBoolean } from '~/lib/utils/common_utils';
export const SIDEBAR_COLLAPSED_CLASS = 'js-sidebar-collapsed';
@@ -112,5 +114,8 @@ export default class ContextualSidebar {
const collapse = parseBoolean(Cookies.get('sidebar_collapsed'));
this.toggleCollapsedSidebar(collapse, true);
}
+
+ initInviteMembersModal();
+ initInviteMembersTrigger();
}
}
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
index 9d4eddc510a..73458a463f2 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
@@ -84,7 +84,7 @@ export default {
),
subnetDropdownHelpPath: 'https://console.aws.amazon.com/vpc/home?#subnets',
securityGroupDropdownHelpText: s__(
- 'ClusterIntegration|Choose the %{linkStart}security group %{linkEnd} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.',
+ 'ClusterIntegration|Choose the %{linkStart}security group%{linkEnd} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.',
),
securityGroupDropdownHelpPath: 'https://console.aws.amazon.com/vpc/home?#securityGroups',
instanceTypesDropdownHelpText: s__(
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index f4a27dc7d1f..ae6e6bf02e4 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -54,6 +54,7 @@ export default class CreateMergeRequestDropdown {
this.isCreatingBranch = false;
this.isCreatingMergeRequest = false;
this.isGettingRef = false;
+ this.refCancelToken = null;
this.mergeRequestCreated = false;
this.refDebounce = debounce((value, target) => this.getRef(value, target), 500);
this.refIsValid = true;
@@ -101,9 +102,18 @@ export default class CreateMergeRequestDropdown {
'click',
this.onClickCreateMergeRequestButton.bind(this),
);
+ this.branchInput.addEventListener('input', this.onChangeInput.bind(this));
this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
+ // Detect for example when user pastes ref using the mouse
+ this.refInput.addEventListener('input', this.onChangeInput.bind(this));
+ // Detect for example when user presses right arrow to apply the suggested ref
this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
+ // Detect when user clicks inside the input to apply the suggested ref
+ this.refInput.addEventListener('click', this.onChangeInput.bind(this));
+ // Detect when user clicks outside the input to apply the suggested ref
+ this.refInput.addEventListener('blur', this.onChangeInput.bind(this));
+ // Detect when user presses tab to apply the suggested ref
this.refInput.addEventListener('keydown', CreateMergeRequestDropdown.processTab.bind(this));
}
@@ -247,8 +257,12 @@ export default class CreateMergeRequestDropdown {
getRef(ref, target = 'all') {
if (!ref) return false;
+ this.refCancelToken = axios.CancelToken.source();
+
return axios
- .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`)
+ .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`, {
+ cancelToken: this.refCancelToken.token,
+ })
.then(({ data }) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
@@ -267,7 +281,10 @@ export default class CreateMergeRequestDropdown {
return this.updateInputState(target, ref, result);
})
- .catch(() => {
+ .catch((thrown) => {
+ if (axios.isCancel(thrown)) {
+ return false;
+ }
this.unavailable();
this.disable();
createFlash({
@@ -325,14 +342,23 @@ export default class CreateMergeRequestDropdown {
let target;
let value;
+ // User changed input, cancel to prevent previous request from interfering
+ if (this.refCancelToken !== null) {
+ this.refCancelToken.cancel();
+ }
+
if (event.target === this.branchInput) {
target = 'branch';
({ value } = this.branchInput);
} else if (event.target === this.refInput) {
target = 'ref';
- value =
- event.target.value.slice(0, event.target.selectionStart) +
- event.target.value.slice(event.target.selectionEnd);
+ if (event.target === document.activeElement) {
+ value =
+ event.target.value.slice(0, event.target.selectionStart) +
+ event.target.value.slice(event.target.selectionEnd);
+ } else {
+ value = event.target.value;
+ }
} else {
return false;
}
@@ -358,6 +384,7 @@ export default class CreateMergeRequestDropdown {
this.enable();
this.showAvailableMessage(target);
+ this.refDebounce(value, target);
return true;
}
@@ -414,7 +441,8 @@ export default class CreateMergeRequestDropdown {
if (!selectedText || this.refInput.dataset.value === this.suggestedRef) return;
event.preventDefault();
- window.getSelection().removeAllRanges();
+ const caretPositionEnd = this.refInput.value.length;
+ this.refInput.setSelectionRange(caretPositionEnd, caretPositionEnd);
}
removeMessage(target) {
diff --git a/app/assets/javascripts/crm/components/contacts_root.vue b/app/assets/javascripts/crm/components/contacts_root.vue
new file mode 100644
index 00000000000..83c02f7d5fe
--- /dev/null
+++ b/app/assets/javascripts/crm/components/contacts_root.vue
@@ -0,0 +1,80 @@
+<script>
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { s__, __ } from '~/locale';
+import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ },
+ inject: ['groupFullPath'],
+ data() {
+ return { contacts: [] };
+ },
+ apollo: {
+ contacts: {
+ query() {
+ return getGroupContactsQuery;
+ },
+ variables() {
+ return {
+ groupFullPath: this.groupFullPath,
+ };
+ },
+ update(data) {
+ return this.extractContacts(data);
+ },
+ error(error) {
+ createFlash({
+ message: __('Something went wrong. Please try again.'),
+ error,
+ captureError: true,
+ });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.contacts.loading;
+ },
+ },
+ methods: {
+ extractContacts(data) {
+ const contacts = data?.group?.contacts?.nodes || [];
+ return contacts.slice().sort((a, b) => a.firstName.localeCompare(b.firstName));
+ },
+ },
+ fields: [
+ { key: 'firstName', sortable: true },
+ { key: 'lastName', sortable: true },
+ { key: 'email', sortable: true },
+ { key: 'phone', sortable: true },
+ { key: 'description', sortable: true },
+ {
+ key: 'organization',
+ formatter: (organization) => {
+ return organization?.name;
+ },
+ sortable: true,
+ },
+ ],
+ i18n: {
+ emptyText: s__('Crm|No contacts found'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
+ <gl-table
+ v-else
+ :items="contacts"
+ :fields="$options.fields"
+ :empty-text="$options.i18n.emptyText"
+ show-empty
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/crm/components/organizations_root.vue b/app/assets/javascripts/crm/components/organizations_root.vue
new file mode 100644
index 00000000000..98b45d0a042
--- /dev/null
+++ b/app/assets/javascripts/crm/components/organizations_root.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { s__, __ } from '~/locale';
+import getGroupOrganizationsQuery from './queries/get_group_organizations.query.graphql';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ },
+ inject: ['groupFullPath'],
+ data() {
+ return { organizations: [] };
+ },
+ apollo: {
+ organizations: {
+ query() {
+ return getGroupOrganizationsQuery;
+ },
+ variables() {
+ return {
+ groupFullPath: this.groupFullPath,
+ };
+ },
+ update(data) {
+ return this.extractOrganizations(data);
+ },
+ error(error) {
+ createFlash({
+ message: __('Something went wrong. Please try again.'),
+ error,
+ captureError: true,
+ });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.organizations.loading;
+ },
+ },
+ methods: {
+ extractOrganizations(data) {
+ const organizations = data?.group?.organizations?.nodes || [];
+ return organizations.slice().sort((a, b) => a.name.localeCompare(b.name));
+ },
+ },
+ fields: [
+ { key: 'name', sortable: true },
+ { key: 'defaultRate', sortable: true },
+ { key: 'description', sortable: true },
+ ],
+ i18n: {
+ emptyText: s__('Crm|No organizations found'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
+ <gl-table
+ v-else
+ :items="organizations"
+ :fields="$options.fields"
+ :empty-text="$options.i18n.emptyText"
+ show-empty
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql b/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql
new file mode 100644
index 00000000000..f6acd258585
--- /dev/null
+++ b/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql
@@ -0,0 +1,22 @@
+query contacts($groupFullPath: ID!) {
+ group(fullPath: $groupFullPath) {
+ __typename
+ id
+ contacts {
+ nodes {
+ __typename
+ id
+ firstName
+ lastName
+ email
+ phone
+ description
+ organization {
+ __typename
+ id
+ name
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql b/app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql
new file mode 100644
index 00000000000..7c4ec6ec585
--- /dev/null
+++ b/app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql
@@ -0,0 +1,15 @@
+query organizations($groupFullPath: ID!) {
+ group(fullPath: $groupFullPath) {
+ __typename
+ id
+ organizations {
+ nodes {
+ __typename
+ id
+ name
+ defaultRate
+ description
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/crm/contacts_bundle.js b/app/assets/javascripts/crm/contacts_bundle.js
new file mode 100644
index 00000000000..6438953596e
--- /dev/null
+++ b/app/assets/javascripts/crm/contacts_bundle.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import CrmContactsRoot from './components/contacts_root.vue';
+
+Vue.use(VueApollo);
+
+export default () => {
+ const el = document.getElementById('js-crm-contacts-app');
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ if (!el) {
+ return false;
+ }
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: { groupFullPath: el.dataset.groupFullPath },
+ render(createElement) {
+ return createElement(CrmContactsRoot);
+ },
+ });
+};
diff --git a/app/assets/javascripts/crm/organizations_bundle.js b/app/assets/javascripts/crm/organizations_bundle.js
new file mode 100644
index 00000000000..ac9990b9fb4
--- /dev/null
+++ b/app/assets/javascripts/crm/organizations_bundle.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import CrmOrganizationsRoot from './components/organizations_root.vue';
+
+Vue.use(VueApollo);
+
+export default () => {
+ const el = document.getElementById('js-crm-organizations-app');
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ if (!el) {
+ return false;
+ }
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: { groupFullPath: el.dataset.groupFullPath },
+ render(createElement) {
+ return createElement(CrmOrganizationsRoot);
+ },
+ });
+};
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index 1d98a42ce58..36430e51dd2 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -2,10 +2,12 @@
import { GlLoadingIcon } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mapActions, mapState, mapGetters } from 'vuex';
+import { toYmd } from '~/analytics/shared/utils';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
+import UrlSync from '~/vue_shared/components/url_sync.vue';
import { __ } from '~/locale';
import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants';
@@ -19,6 +21,7 @@ export default {
StageTable,
ValueStreamFilters,
ValueStreamMetrics,
+ UrlSync,
},
props: {
noDataSvgPath: {
@@ -54,6 +57,9 @@ export default {
'pagination',
]),
...mapGetters(['pathNavigationData', 'filterParams']),
+ isLoaded() {
+ return !this.isLoading && !this.isLoadingStage;
+ },
displayStageEvents() {
const { selectedStageEvents, isLoadingStage, isEmptyStage } = this;
return selectedStageEvents.length && !isLoadingStage && !isEmptyStage;
@@ -98,6 +104,16 @@ export default {
metricsRequests() {
return this.features?.cycleAnalyticsForGroups ? METRICS_REQUESTS : SUMMARY_METRICS_REQUEST;
},
+ query() {
+ return {
+ created_after: toYmd(this.createdAfter),
+ created_before: toYmd(this.createdBefore),
+ stage_id: this.selectedStage?.id || null,
+ sort: this.pagination?.sort || null,
+ direction: this.pagination?.direction || null,
+ page: this.pagination?.page || null,
+ };
+ },
},
methods: {
...mapActions([
@@ -176,5 +192,6 @@ export default {
:pagination="pagination"
@handleUpdatePagination="onHandleUpdatePagination"
/>
+ <url-sync v-if="isLoaded" :query="query" />
</div>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/metric_popover.vue b/app/assets/javascripts/cycle_analytics/components/metric_popover.vue
new file mode 100644
index 00000000000..8d90e7b2392
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/metric_popover.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlPopover, GlLink, GlIcon } from '@gitlab/ui';
+
+export default {
+ name: 'MetricPopover',
+ components: {
+ GlPopover,
+ GlLink,
+ GlIcon,
+ },
+ props: {
+ metric: {
+ type: Object,
+ required: true,
+ },
+ target: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ metricLinks() {
+ return this.metric.links?.filter((link) => !link.docs_link) || [];
+ },
+ docsLink() {
+ return this.metric.links?.find((link) => link.docs_link);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-popover :target="target" placement="bottom">
+ <template #title>
+ <span class="gl-display-block gl-text-left" data-testid="metric-label">{{
+ metric.label
+ }}</span>
+ </template>
+ <div
+ v-for="(link, idx) in metricLinks"
+ :key="`link-${idx}`"
+ class="gl-display-flex gl-justify-content-space-between gl-text-right gl-py-1"
+ data-testid="metric-link"
+ >
+ <span>{{ link.label }}</span>
+ <gl-link :href="link.url" class="gl-font-sm">
+ {{ link.name }}
+ </gl-link>
+ </div>
+ <span v-if="metric.description" data-testid="metric-description">{{ metric.description }}</span>
+ <gl-link
+ v-if="docsLink"
+ :href="docsLink.url"
+ class="gl-font-sm"
+ target="_blank"
+ data-testid="metric-docs-link"
+ >{{ docsLink.label }}
+ <gl-icon name="external-link" class="gl-vertical-align-middle" />
+ </gl-link>
+ </gl-popover>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue b/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
index 7371ffd2c7c..9671742e564 100644
--- a/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
+++ b/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
@@ -1,11 +1,13 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlPopover } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { flatten } from 'lodash';
import createFlash from '~/flash';
import { sprintf, s__ } from '~/locale';
+import { redirectTo } from '~/lib/utils/url_utility';
import { METRICS_POPOVER_CONTENT } from '../constants';
import { removeFlash, prepareTimeMetricsData } from '../utils';
+import MetricPopover from './metric_popover.vue';
const requestData = ({ request, endpoint, path, params, name }) => {
return request({ endpoint, params, requestPath: path })
@@ -31,9 +33,9 @@ const fetchMetricsData = (reqs = [], path, params) => {
export default {
name: 'ValueStreamMetrics',
components: {
- GlPopover,
GlSingleStat,
GlSkeletonLoading,
+ MetricPopover,
},
props: {
requestPath: {
@@ -76,32 +78,33 @@ export default {
this.isLoading = false;
});
},
+ hasLinks(links) {
+ return links?.length && links[0].url;
+ },
+ clickHandler({ links }) {
+ if (this.hasLinks(links)) {
+ redirectTo(links[0].url);
+ }
+ },
},
};
</script>
<template>
<div class="gl-display-flex gl-flex-wrap" data-testid="vsa-time-metrics">
- <div v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6">
- <gl-skeleton-loading />
+ <gl-skeleton-loading v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6" />
+ <div v-for="metric in metrics" v-show="!isLoading" :key="metric.key" class="gl-my-6 gl-pr-9">
+ <gl-single-stat
+ :id="metric.key"
+ :value="`${metric.value}`"
+ :title="metric.label"
+ :unit="metric.unit || ''"
+ :should-animate="true"
+ :animation-decimal-places="1"
+ :class="{ 'gl-hover-cursor-pointer': hasLinks(metric.links) }"
+ tabindex="0"
+ @click="clickHandler(metric)"
+ />
+ <metric-popover :metric="metric" :target="metric.key" />
</div>
- <template v-else>
- <div v-for="metric in metrics" :key="metric.key" class="gl-my-6 gl-pr-9">
- <gl-single-stat
- :id="metric.key"
- :value="`${metric.value}`"
- :title="metric.label"
- :unit="metric.unit || ''"
- :should-animate="true"
- :animation-decimal-places="1"
- tabindex="0"
- />
- <gl-popover :target="metric.key" placement="bottom">
- <template #title>
- <span class="gl-display-block gl-text-left">{{ metric.label }}</span>
- </template>
- <span v-if="metric.description">{{ metric.description }}</span>
- </gl-popover>
- </div>
- </template>
</div>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js
index c205aa1e831..7d5822b0824 100644
--- a/app/assets/javascripts/cycle_analytics/constants.js
+++ b/app/assets/javascripts/cycle_analytics/constants.js
@@ -5,8 +5,6 @@ import {
} from '~/api/analytics_api';
import { __, s__ } from '~/locale';
-export const DEFAULT_DAYS_IN_PAST = 30;
-export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview';
export const DEFAULT_VALUE_STREAM = {
@@ -47,6 +45,11 @@ export const METRICS_POPOVER_CONTENT = {
"ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.",
),
},
+ 'lead-time-for-changes': {
+ description: s__(
+ 'ValueStreamAnalytics|Median time between merge request merge and deployment to a production environment for all MRs deployed in the given time period.',
+ ),
+ },
'new-issue': { description: s__('ValueStreamAnalytics|Number of new issues created.') },
'new-issues': { description: s__('ValueStreamAnalytics|Number of new issues created.') },
deploys: { description: s__('ValueStreamAnalytics|Total number of deploys to production.') },
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js
index 34ef03409b8..3da8696edeb 100644
--- a/app/assets/javascripts/cycle_analytics/index.js
+++ b/app/assets/javascripts/cycle_analytics/index.js
@@ -1,44 +1,36 @@
import Vue from 'vue';
+import {
+ extractFilterQueryParameters,
+ extractPaginationQueryParameters,
+} from '~/analytics/shared/utils';
import Translate from '../vue_shared/translate';
import CycleAnalytics from './components/base.vue';
-import { DEFAULT_DAYS_TO_DISPLAY } from './constants';
import createStore from './store';
-import { calculateFormattedDayInPast } from './utils';
+import { buildCycleAnalyticsInitialData } from './utils';
Vue.use(Translate);
export default () => {
const store = createStore();
const el = document.querySelector('#js-cycle-analytics');
- const {
- noAccessSvgPath,
- noDataSvgPath,
- requestPath,
- fullPath,
- projectId,
- groupId,
- groupPath,
- labelsPath,
- milestonesPath,
- } = el.dataset;
+ const { noAccessSvgPath, noDataSvgPath } = el.dataset;
+ const initialData = buildCycleAnalyticsInitialData({ ...el.dataset, gon });
- const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
+ const pagination = extractPaginationQueryParameters(window.location.search);
+ const {
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ } = extractFilterQueryParameters(window.location.search);
store.dispatch('initializeVsa', {
- projectId: parseInt(projectId, 10),
- endpoints: {
- requestPath,
- fullPath,
- labelsPath,
- milestonesPath,
- groupId: parseInt(groupId, 10),
- groupPath,
- },
- features: {
- cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
- },
- createdBefore: new Date(now),
- createdAfter: new Date(past),
+ ...initialData,
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ pagination,
});
// eslint-disable-next-line no-new
@@ -52,7 +44,6 @@ export default () => {
props: {
noDataSvgPath,
noAccessSvgPath,
- fullPath,
},
}),
});
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js
index 24b62849db7..e0156b24f9d 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/cycle_analytics/store/actions.js
@@ -14,7 +14,7 @@ import * as types from './mutation_types';
export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
commit(types.SET_SELECTED_VALUE_STREAM, valueStream);
- return Promise.all([dispatch('fetchValueStreamStages'), dispatch('fetchCycleAnalyticsData')]);
+ return dispatch('fetchValueStreamStages');
};
export const fetchValueStreamStages = ({ commit, state }) => {
@@ -46,10 +46,8 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => {
} = state;
commit(types.REQUEST_VALUE_STREAMS);
- const stageRequests = ['setSelectedStage', 'fetchStageMedians', 'fetchStageCountValues'];
return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
- .then(() => Promise.all(stageRequests.map((r) => dispatch(r))))
.catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
});
@@ -153,33 +151,36 @@ export const fetchStageCountValues = ({
});
};
-export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => {
- const stage = selectedStage || stages[0];
- commit(types.SET_SELECTED_STAGE, stage);
- return dispatch('fetchStageData');
+export const fetchValueStreamStageData = ({ dispatch }) =>
+ Promise.all([
+ dispatch('fetchCycleAnalyticsData'),
+ dispatch('fetchStageData'),
+ dispatch('fetchStageMedians'),
+ dispatch('fetchStageCountValues'),
+ ]);
+
+export const refetchStageData = async ({ dispatch, commit }) => {
+ commit(types.SET_LOADING, true);
+ await dispatch('fetchValueStreamStageData');
+ commit(types.SET_LOADING, false);
};
-export const setLoading = ({ commit }, value) => commit(types.SET_LOADING, value);
-
-const refetchStageData = (dispatch) => {
- return Promise.resolve()
- .then(() => dispatch('setLoading', true))
- .then(() =>
- Promise.all([
- dispatch('fetchCycleAnalyticsData'),
- dispatch('fetchStageData'),
- dispatch('fetchStageMedians'),
- dispatch('fetchStageCountValues'),
- ]),
- )
- .finally(() => dispatch('setLoading', false));
+export const setSelectedStage = ({ dispatch, commit }, selectedStage = null) => {
+ commit(types.SET_SELECTED_STAGE, selectedStage);
+ return dispatch('refetchStageData');
};
-export const setFilters = ({ dispatch }) => refetchStageData(dispatch);
+export const setFilters = ({ dispatch }) => dispatch('refetchStageData');
export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore }) => {
commit(types.SET_DATE_RANGE, { createdAfter, createdBefore });
- return refetchStageData(dispatch);
+ return dispatch('refetchStageData');
+};
+
+export const setInitialStage = ({ dispatch, commit, state: { stages } }, stage) => {
+ const selectedStage = stage || stages[0];
+ commit(types.SET_SELECTED_STAGE, selectedStage);
+ return dispatch('fetchValueStreamStageData');
};
export const updateStageTablePagination = (
@@ -190,12 +191,18 @@ export const updateStageTablePagination = (
return dispatch('fetchStageData', selectedStage.id);
};
-export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
+export const initializeVsa = async ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData);
const {
endpoints: { fullPath, groupPath, milestonesPath = '', labelsPath = '' },
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ selectedStage = null,
} = initialData;
+
dispatch('filters/setEndpoints', {
labelsEndpoint: labelsPath,
milestonesEndpoint: milestonesPath,
@@ -203,7 +210,15 @@ export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
projectEndpoint: fullPath,
});
- return dispatch('setLoading', true)
- .then(() => dispatch('fetchValueStreams'))
- .finally(() => dispatch('setLoading', false));
+ dispatch('filters/initialize', {
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ });
+
+ commit(types.SET_LOADING, true);
+ await dispatch('fetchValueStreams');
+ await dispatch('setInitialStage', selectedStage);
+ commit(types.SET_LOADING, false);
};
diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/cycle_analytics/utils.js
index 3c6267bac06..9af63f5f9cc 100644
--- a/app/assets/javascripts/cycle_analytics/utils.js
+++ b/app/assets/javascripts/cycle_analytics/utils.js
@@ -1,7 +1,4 @@
-import dateFormat from 'dateformat';
-import { dateFormats } from '~/analytics/shared/constants';
import { hideFlash } from '~/flash';
-import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
import { parseSeconds } from '~/lib/utils/datetime_utility';
import { formatTimeAsSummary } from '~/lib/utils/datetime/date_format_utility';
import { slugify } from '~/lib/utils/text_utility';
@@ -74,23 +71,6 @@ export const formatMedianValues = (medians = []) =>
export const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
stages.filter(({ hidden = false }) => hidden === isHidden);
-const toIsoFormat = (d) => dateFormat(d, dateFormats.isoDate);
-
-/**
- * Takes an integer specifying the number of days to subtract
- * from the date specified will return the 2 dates, formatted as ISO dates
- *
- * @param {Number} daysInPast - Number of days in the past to subtract
- * @param {Date} [today=new Date] - Date to subtract days from, defaults to today
- * @returns {Object} Returns 'now' and the 'past' date formatted as ISO dates
- */
-export const calculateFormattedDayInPast = (daysInPast, today = new Date()) => {
- return {
- now: toIsoFormat(today),
- past: toIsoFormat(getDateInPast(today, daysInPast)),
- };
-};
-
/**
* @typedef {Object} MetricData
* @property {String} title - Title of the metric measured
@@ -123,3 +103,43 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) =>
description: popoverContent[key]?.description || '',
};
});
+
+const extractFeatures = (gon) => ({
+ cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
+});
+
+/**
+ * Builds the initial data object for Value Stream Analytics with data loaded from the backend
+ *
+ * @param {Object} dataset - dataset object paseed to the frontend via data-* properties
+ * @returns {Object} - The initial data to load the app with
+ */
+export const buildCycleAnalyticsInitialData = ({
+ fullPath,
+ requestPath,
+ projectId,
+ groupId,
+ groupPath,
+ labelsPath,
+ milestonesPath,
+ stage,
+ createdAfter,
+ createdBefore,
+ gon,
+} = {}) => {
+ return {
+ projectId: parseInt(projectId, 10),
+ endpoints: {
+ requestPath,
+ fullPath,
+ labelsPath,
+ milestonesPath,
+ groupId: parseInt(groupId, 10),
+ groupPath,
+ },
+ createdAfter: new Date(createdAfter),
+ createdBefore: new Date(createdBefore),
+ selectedStage: stage ? JSON.parse(stage) : null,
+ features: extractFeatures(gon),
+ };
+};
diff --git a/app/assets/javascripts/deploy_tokens/components/revoke_button.vue b/app/assets/javascripts/deploy_tokens/components/revoke_button.vue
index e026391ae22..fdf8b7796bf 100644
--- a/app/assets/javascripts/deploy_tokens/components/revoke_button.vue
+++ b/app/assets/javascripts/deploy_tokens/components/revoke_button.vue
@@ -62,7 +62,7 @@ export default {
</gl-sprintf>
{{ s__('DeployTokens|This action cannot be undone.') }}
<template #modal-footer>
- <gl-button category="secondary" @click="cancelHandler">{{ s__('Cancel') }}</gl-button>
+ <gl-button category="secondary" @click="cancelHandler">{{ __('Cancel') }}</gl-button>
<gl-button
category="primary"
variant="danger"
diff --git a/app/assets/javascripts/design_management/graphql.js b/app/assets/javascripts/design_management/graphql.js
index fa57537f74e..5cf32cb7fe3 100644
--- a/app/assets/javascripts/design_management/graphql.js
+++ b/app/assets/javascripts/design_management/graphql.js
@@ -88,7 +88,6 @@ const defaultClient = createDefaultClient(
fragmentMatcher,
},
typeDefs,
- assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 5092c30aa60..42d5d8fb359 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -4,7 +4,7 @@ import VueDraggable from 'vuedraggable';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
-import { getFilename } from '~/lib/utils/file_upload';
+import { getFilename, validateImageName } from '~/lib/utils/file_upload';
import { __, s__, sprintf } from '~/locale';
import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import DeleteButton from '../components/delete_button.vue';
@@ -284,12 +284,16 @@ export default {
return;
}
event.preventDefault();
- let filename = getFilename(event);
- if (!filename || filename === 'image.png') {
- filename = `design_${Date.now()}.png`;
- }
- const newFile = new File([files[0]], filename);
- this.onUploadDesign([newFile]);
+ const fileList = [...files];
+ fileList.forEach((file) => {
+ let filename = getFilename(file);
+ filename = validateImageName(file);
+ if (!filename || filename === 'image.png') {
+ filename = `design_${Date.now()}.png`;
+ }
+ const newFile = new File([file], filename);
+ this.onUploadDesign([newFile]);
+ });
}
},
toggleOnPasteListener() {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 465f9836140..f405b82b05b 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -44,6 +44,7 @@ import {
TRACKING_MULTIPLE_FILES_MODE,
} from '../constants';
+import { discussionIntersectionObserverHandlerFactory } from '../utils/discussions';
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
@@ -86,6 +87,9 @@ export default {
ALERT_MERGE_CONFLICT,
ALERT_COLLAPSED_FILES,
},
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
props: {
endpoint: {
type: String,
@@ -392,8 +396,6 @@ export default {
diffsApp.instrument();
},
created() {
- this.mergeRequestContainers = document.querySelectorAll('.merge-request-container');
-
this.adjustView();
this.subscribeToEvents();
@@ -521,13 +523,6 @@ export default {
} else {
this.removeEventListeners();
}
-
- if (!this.isFluidLayout && this.glFeatures.mrChangesFluidLayout) {
- this.mergeRequestContainers.forEach((el) => {
- el.classList.toggle('limit-container-width', !this.shouldShow);
- el.classList.toggle('container-limited', !this.shouldShow);
- });
- }
},
setEventListeners() {
Mousetrap.bind(keysFor(MR_PREVIOUS_FILE_IN_DIFF), () => this.jumpToFile(-1));
@@ -579,7 +574,7 @@ export default {
jumpToFile(step) {
const targetIndex = this.currentDiffIndex + step;
if (targetIndex >= 0 && targetIndex < this.diffFiles.length) {
- this.scrollToFile(this.diffFiles[targetIndex].file_path);
+ this.scrollToFile({ path: this.diffFiles[targetIndex].file_path });
}
},
setTreeDisplay() {
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 4435a533591..e54fde72847 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButtonGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlButtonGroup, GlButton, GlTooltipDirective, GlSafeHtmlDirective } from '@gitlab/ui';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@@ -34,6 +34,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -88,6 +89,9 @@ export default {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
});
},
+ safeHtmlConfig: {
+ ADD_TAGS: ['gl-emoji'],
+ },
};
</script>
@@ -101,7 +105,7 @@ export default {
>
<div
v-if="commit.signature_html"
- v-html="commit.signature_html /* eslint-disable-line vue/no-v-html */"
+ v-safe-html:[$options.safeHtmlConfig]="commit.signature_html"
></div>
<commit-pipeline-status
v-if="commit.pipeline_status_path"
@@ -142,9 +146,9 @@ export default {
<div class="commit-detail flex-list">
<div class="commit-content" data-qa-selector="commit_content">
<a
+ v-safe-html:[$options.safeHtmlConfig]="commit.title_html"
:href="commit.commit_url"
class="commit-row-message item-title"
- v-html="commit.title_html /* eslint-disable-line vue/no-v-html */"
></a>
<span class="commit-row-message d-block d-sm-none">&middot; {{ commit.short_id }}</span>
@@ -174,9 +178,9 @@ export default {
<div>
<pre
v-if="commit.description_html"
+ v-safe-html:[$options.safeHtmlConfig]="commitDescription"
:class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
class="commit-row-description gl-mb-3 gl-text-body"
- v-html="commitDescription /* eslint-disable-line vue/no-v-html */"
></pre>
</div>
</li>
diff --git a/app/assets/javascripts/diffs/components/diff_comment_cell.vue b/app/assets/javascripts/diffs/components/diff_comment_cell.vue
index 4af4b46f94c..a4fae652d02 100644
--- a/app/assets/javascripts/diffs/components/diff_comment_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_comment_cell.vue
@@ -29,6 +29,11 @@ export default {
required: false,
default: false,
},
+ lineRange: {
+ type: Object,
+ required: false,
+ default: null,
+ },
linePosition: {
type: String,
required: false,
@@ -59,6 +64,7 @@ export default {
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line"
+ :range="lineRange"
:note-target-line="line"
:help-page-path="helpPagePath"
:line-position="linePosition"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 4bcb99424db..238f07ac22c 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -14,7 +14,6 @@ import {
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { IdState } from 'vendor/vue-virtual-scroller';
-import { diffViewerModes } from '~/ide/constants';
import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
@@ -50,7 +49,7 @@ export default {
mixins: [glFeatureFlagsMixin(), IdState({ idProp: (vm) => vm.diffFile.file_hash })],
i18n: {
...DIFF_FILE_HEADER,
- compareButtonLabel: s__('Compare submodule commit revisions'),
+ compareButtonLabel: __('Compare submodule commit revisions'),
},
props: {
discussionPath: {
@@ -130,7 +129,7 @@ export default {
const truncatedOldSha = escape(truncateSha(this.diffFile.submodule_compare.old_sha));
const truncatedNewSha = escape(truncateSha(this.diffFile.submodule_compare.new_sha));
return sprintf(
- s__('Compare %{oldCommitId}...%{newCommitId}'),
+ __('Compare %{oldCommitId}...%{newCommitId}'),
{
oldCommitId: `<span class="commit-sha">${truncatedOldSha}</span>`,
newCommitId: `<span class="commit-sha">${truncatedNewSha}</span>`,
@@ -181,7 +180,7 @@ export default {
return this.diffFile.renamed_file;
},
isModeChanged() {
- return this.diffFile.viewer.name === diffViewerModes.mode_changed;
+ return this.diffFile.mode_changed;
},
expandDiffToFullFileTitle() {
if (this.diffFile.isShowingFullFile) {
@@ -221,7 +220,7 @@ export default {
'toggleFileDiscussions',
'toggleFileDiscussionWrappers',
'toggleFullDiff',
- 'toggleActiveFileByHash',
+ 'setCurrentFileHash',
'reviewFile',
'setFileCollapsedByUser',
]),
@@ -244,7 +243,7 @@ export default {
scrollToElement(document.querySelector(selector));
window.location.hash = selector;
if (!this.viewDiffsFileByFile) {
- this.toggleActiveFileByHash(this.diffFile.file_hash);
+ this.setCurrentFileHash(this.diffFile.file_hash);
}
}
},
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index c445989f143..9d355c96af1 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -2,6 +2,7 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import { s__ } from '~/locale';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MultilineCommentForm from '../../notes/components/multiline_comment_form.vue';
import {
@@ -32,6 +33,11 @@ export default {
type: Object,
required: true,
},
+ range: {
+ type: Object,
+ required: false,
+ default: null,
+ },
linePosition: {
type: String,
required: false,
@@ -49,6 +55,7 @@ export default {
},
data() {
return {
+ lines: null,
commentLineStart: {
line_code: this.line.line_code,
type: this.line.type,
@@ -116,10 +123,8 @@ export default {
return commentLineOptions(lines, this.line, this.line.line_code, side);
},
commentLines() {
- if (!this.selectedCommentPosition) return [];
-
const lines = [];
- const { start, end } = this.selectedCommentPosition;
+ const { start, end } = this.lines;
const diffLines = this.diffFile[INLINE_DIFF_LINES_KEY];
let isAdding = false;
@@ -144,6 +149,13 @@ export default {
return lines;
},
},
+ created() {
+ if (this.range) {
+ this.lines = { ...this.range };
+ } else if (this.line) {
+ this.lines = { start: this.line, end: this.line };
+ }
+ },
mounted() {
if (this.isLoggedIn) {
const keys = [
@@ -166,16 +178,16 @@ export default {
'saveDiffDiscussion',
'setSuggestPopoverDismissed',
]),
- handleCancelCommentForm(shouldConfirm, isDirty) {
+ async handleCancelCommentForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
- // eslint-disable-next-line no-alert
- if (!window.confirm(msg)) {
+ const confirmed = await confirmAction(msg);
+
+ if (!confirmed) {
return;
}
}
-
this.cancelCommentForm({
lineCode: this.line.line_code,
fileHash: this.diffFileHash,
@@ -189,6 +201,9 @@ export default {
this.handleCancelCommentForm(),
);
},
+ updateStartLine(line) {
+ this.lines.start = line;
+ },
},
};
</script>
@@ -199,7 +214,9 @@ export default {
<multiline-comment-form
v-model="commentLineStart"
:line="line"
+ :line-range="lines"
:comment-line-options="commentLineOptions"
+ @input="updateStartLine"
/>
</div>
<note-form
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 64ded1ca8ca..55c796182ee 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -6,6 +6,7 @@ import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import { hide } from '~/tooltips';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { pickDirection } from '../utils/diff_line';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
import DiffRow from './diff_row.vue';
@@ -106,6 +107,16 @@ export default {
});
this.idState.dragStart = null;
},
+ singleLineComment(code, line) {
+ const lineDir = pickDirection({ line, code });
+
+ this.idState.updatedLineRange = {
+ start: lineDir,
+ end: lineDir,
+ };
+
+ this.showCommentForm({ lineCode: lineDir.line_code, fileHash: this.diffFile.file_hash });
+ },
isHighlighted(line) {
return isHighlighted(
this.highlightedRow,
@@ -169,7 +180,7 @@ export default {
:index="index"
:is-highlighted="isHighlighted(line)"
:file-line-coverage="fileLineCoverage"
- @showCommentForm="(lineCode) => showCommentForm({ lineCode, fileHash: diffFile.file_hash })"
+ @showCommentForm="(code) => singleLineComment(code, line)"
@setHighlightedRow="setHighlightedRow"
@toggleLineDiscussions="
({ lineCode, expanded }) =>
@@ -193,6 +204,7 @@ export default {
<diff-comment-cell
v-if="line.left && (line.left.renderDiscussion || line.left.hasCommentForm)"
:line="line.left"
+ :line-range="idState.updatedLineRange"
:diff-file-hash="diffFile.file_hash"
:help-page-path="helpPagePath"
line-position="left"
@@ -206,6 +218,7 @@ export default {
<diff-comment-cell
v-if="line.right && (line.right.renderDiscussion || line.right.hasCommentForm)"
:line="line.right"
+ :line-range="idState.updatedLineRange"
:diff-file-hash="diffFile.file_hash"
:line-index="index"
:help-page-path="helpPagePath"
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 41d885d3dc1..85e4199d1c1 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -98,7 +98,7 @@ export default {
:file-row-component="$options.DiffFileRow"
:current-diff-file-id="currentDiffFileId"
@toggleTreeOpen="toggleTreeOpen"
- @clickFile="scrollToFile"
+ @clickFile="(path) => scrollToFile({ path })"
/>
</template>
<p v-else class="prepend-top-20 append-bottom-20 text-center">
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 1b1ab59b2b4..260ebdf2141 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -138,7 +138,7 @@ export default function initDiffsApp(store) {
...mapActions('diffs', ['toggleFileFinder', 'scrollToFile']),
openFile(file) {
window.mrTabs.tabShown('diffs');
- this.scrollToFile(file.path);
+ this.scrollToFile({ path: file.path });
},
},
render(createElement) {
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 5c94c6b803b..692cb913a57 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -85,6 +85,12 @@ export const setBaseConfig = ({ commit }, options) => {
viewDiffsFileByFile,
mrReviews,
});
+
+ Array.from(new Set(Object.values(mrReviews).flat())).forEach((id) => {
+ const viewedId = id.replace(/^hash:/, '');
+
+ commit(types.SET_DIFF_FILE_VIEWED, { id: viewedId, seen: true });
+ });
};
export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
@@ -127,7 +133,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
}
if (!isNoteLink && !state.currentDiffFileId) {
- commit(types.VIEW_DIFF_FILE, diff_files[0]?.file_hash);
+ commit(types.SET_CURRENT_DIFF_FILE, diff_files[0]?.file_hash);
}
if (isNoteLink) {
@@ -143,7 +149,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
!state.diffFiles.some((f) => f.file_hash === state.currentDiffFileId) &&
!isNoteLink
) {
- commit(types.VIEW_DIFF_FILE, state.diffFiles[0].file_hash);
+ commit(types.SET_CURRENT_DIFF_FILE, state.diffFiles[0].file_hash);
}
if (state.diffFiles?.length) {
@@ -248,7 +254,7 @@ export const fetchCoverageFiles = ({ commit, state }) => {
export const setHighlightedRow = ({ commit }, lineCode) => {
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
- commit(types.VIEW_DIFF_FILE, fileHash);
+ commit(types.SET_CURRENT_DIFF_FILE, fileHash);
handleLocationHash();
};
@@ -514,23 +520,25 @@ export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_FOLDER_OPEN, path);
};
-export const toggleActiveFileByHash = ({ commit }, hash) => {
- commit(types.VIEW_DIFF_FILE, hash);
+export const setCurrentFileHash = ({ commit }, hash) => {
+ commit(types.SET_CURRENT_DIFF_FILE, hash);
};
-export const scrollToFile = ({ state, commit, getters }, path) => {
+export const scrollToFile = ({ state, commit, getters }, { path, setHash = true }) => {
if (!state.treeEntries[path]) return;
const { fileHash } = state.treeEntries[path];
- commit(types.VIEW_DIFF_FILE, fileHash);
+ commit(types.SET_CURRENT_DIFF_FILE, fileHash);
if (getters.isVirtualScrollingEnabled) {
eventHub.$emit('scrollToFileHash', fileHash);
- setTimeout(() => {
- window.history.replaceState(null, null, `#${fileHash}`);
- });
+ if (setHash) {
+ setTimeout(() => {
+ window.history.replaceState(null, null, `#${fileHash}`);
+ });
+ }
} else {
document.location.hash = fileHash;
@@ -804,7 +812,7 @@ export const setCurrentDiffFileIdFromNote = ({ commit, state, rootGetters }, not
const fileHash = rootGetters.getDiscussion(note.discussion_id).diff_file?.file_hash;
if (fileHash && state.diffFiles.some((f) => f.file_hash === fileHash)) {
- commit(types.VIEW_DIFF_FILE, fileHash);
+ commit(types.SET_CURRENT_DIFF_FILE, fileHash);
}
};
@@ -812,7 +820,7 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => {
const fileHash = state.diffFiles[index].file_hash;
document.location.hash = fileHash;
- commit(types.VIEW_DIFF_FILE, fileHash);
+ commit(types.SET_CURRENT_DIFF_FILE, fileHash);
};
export const setFileByFile = ({ state, commit }, { fileByFile }) => {
@@ -848,6 +856,8 @@ export function reviewFile({ commit, state }, { file, reviewed = true }) {
const reviews = markFileReview(state.mrReviews, file, reviewed);
setReviewsForMergeRequest(mrPath, reviews);
+
+ commit(types.SET_DIFF_FILE_VIEWED, { id: file.file_hash, seen: reviewed });
commit(types.SET_MR_FILE_REVIEWS, reviews);
}
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 60836f747f5..51c21c1bfc4 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -20,7 +20,8 @@ export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
export const SET_SHOW_TREE_LIST = 'SET_SHOW_TREE_LIST';
-export const VIEW_DIFF_FILE = 'VIEW_DIFF_FILE';
+export const SET_CURRENT_DIFF_FILE = 'SET_CURRENT_DIFF_FILE';
+export const SET_DIFF_FILE_VIEWED = 'SET_DIFF_FILE_VIEWED';
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 6bc927b9d1f..4a9df0eafcc 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -254,9 +254,11 @@ export default {
[types.SET_SHOW_TREE_LIST](state, showTreeList) {
state.showTreeList = showTreeList;
},
- [types.VIEW_DIFF_FILE](state, fileId) {
+ [types.SET_CURRENT_DIFF_FILE](state, fileId) {
state.currentDiffFileId = fileId;
- Vue.set(state.viewedDiffFileIds, fileId, true);
+ },
+ [types.SET_DIFF_FILE_VIEWED](state, { id, seen }) {
+ Vue.set(state.viewedDiffFileIds, id, seen);
},
[types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) {
state.commentForms.push({
diff --git a/app/assets/javascripts/diffs/utils/diff_line.js b/app/assets/javascripts/diffs/utils/diff_line.js
new file mode 100644
index 00000000000..a248cc6318b
--- /dev/null
+++ b/app/assets/javascripts/diffs/utils/diff_line.js
@@ -0,0 +1,10 @@
+export function pickDirection({ line, code } = {}) {
+ const { left, right } = line;
+ let direction = left || right;
+
+ if (right?.line_code === code) {
+ direction = right;
+ }
+
+ return direction;
+}
diff --git a/app/assets/javascripts/diffs/utils/discussions.js b/app/assets/javascripts/diffs/utils/discussions.js
new file mode 100644
index 00000000000..c404705d209
--- /dev/null
+++ b/app/assets/javascripts/diffs/utils/discussions.js
@@ -0,0 +1,76 @@
+function normalize(processable) {
+ const { entry } = processable;
+ const offset = entry.rootBounds.bottom - entry.boundingClientRect.top;
+ const direction =
+ offset < 0 ? 'Up' : 'Down'; /* eslint-disable-line @gitlab/require-i18n-strings */
+
+ return {
+ ...processable,
+ entry: {
+ time: entry.time,
+ type: entry.isIntersecting ? 'intersection' : `scroll${direction}`,
+ },
+ };
+}
+
+function sort({ entry: alpha }, { entry: beta }) {
+ const diff = alpha.time - beta.time;
+ let order = 0;
+
+ if (diff < 0) {
+ order = -1;
+ } else if (diff > 0) {
+ order = 1;
+ } else if (alpha.type === 'intersection' && beta.type === 'scrollUp') {
+ order = 2;
+ } else if (alpha.type === 'scrollUp' && beta.type === 'intersection') {
+ order = -2;
+ }
+
+ return order;
+}
+
+function filter(entry) {
+ return entry.type !== 'scrollDown';
+}
+
+export function discussionIntersectionObserverHandlerFactory() {
+ let unprocessed = [];
+ let timer = null;
+
+ return (processable) => {
+ unprocessed.push(processable);
+
+ if (timer) {
+ clearTimeout(timer);
+ }
+
+ timer = setTimeout(() => {
+ unprocessed
+ .map(normalize)
+ .filter(filter)
+ .sort(sort)
+ .forEach((discussionObservationContainer) => {
+ const {
+ entry: { type },
+ currentDiscussion,
+ isFirstUnresolved,
+ isDiffsPage,
+ functions: { setCurrentDiscussionId, getPreviousUnresolvedDiscussionId },
+ } = discussionObservationContainer;
+
+ if (type === 'intersection') {
+ setCurrentDiscussionId(currentDiscussion.id);
+ } else if (type === 'scrollUp') {
+ setCurrentDiscussionId(
+ isFirstUnresolved
+ ? null
+ : getPreviousUnresolvedDiscussionId(currentDiscussion.id, isDiffsPage),
+ );
+ }
+ });
+
+ unprocessed = [];
+ }, 0);
+ };
+}
diff --git a/app/assets/javascripts/diffs/utils/file_reviews.js b/app/assets/javascripts/diffs/utils/file_reviews.js
index 7a4b1aa6b17..227be4e4a6c 100644
--- a/app/assets/javascripts/diffs/utils/file_reviews.js
+++ b/app/assets/javascripts/diffs/utils/file_reviews.js
@@ -52,8 +52,10 @@ export function markFileReview(reviews, file, reviewed = true) {
if (reviewed) {
fileReviews.add(file.id);
+ fileReviews.add(`hash:${file.file_hash}`);
} else {
fileReviews.delete(file.id);
+ fileReviews.delete(`hash:${file.file_hash}`);
}
updatedReviews[file.file_identifier_hash] = Array.from(fileReviews);
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index f98f63529fc..f404fa4e0e8 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -43,7 +43,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
let pasteText;
let addFileToForm;
let updateAttachingMessage;
- let isImage;
let uploadFile;
formTextarea.wrap('<div class="div-dropzone"></div>');
@@ -173,7 +172,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
return dropzoneInstance.addFile(file);
});
});
- // eslint-disable-next-line consistent-return
+
handlePaste = (event) => {
const pasteEvent = event.originalEvent;
const { clipboardData } = pasteEvent;
@@ -186,32 +185,22 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
const text = converter.convertToTableMarkdown();
pasteText(text);
} else {
- const image = isImage(pasteEvent);
-
- if (image) {
- event.preventDefault();
- const MAX_FILE_NAME_LENGTH = 246;
- const filename = getFilename(pasteEvent) || 'image.png';
- const truncateFilename = truncate(filename, MAX_FILE_NAME_LENGTH);
- const text = `{{${truncateFilename}}}`;
- pasteText(text);
-
- return uploadFile(image.getAsFile(), truncateFilename);
- }
- }
- }
- };
-
- isImage = (data) => {
- let i = 0;
- while (i < data.clipboardData.items.length) {
- const item = data.clipboardData.items[i];
- if (item.type.indexOf('image') !== -1) {
- return item;
+ const fileList = [...clipboardData.files];
+ fileList.forEach((file) => {
+ if (file.type.indexOf('image') !== -1) {
+ event.preventDefault();
+ const MAX_FILE_NAME_LENGTH = 246;
+
+ const filename = getFilename(file) || 'image.png';
+ const truncateFilename = truncate(filename, MAX_FILE_NAME_LENGTH);
+ const text = `{{${truncateFilename}}}`;
+ pasteText(text);
+
+ uploadFile(file, truncateFilename);
+ }
+ });
}
- i += 1;
}
- return false;
};
pasteText = (text, shouldPad) => {
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index d40d19000fb..e855e304d27 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -1,17 +1,9 @@
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { __ } from '~/locale';
-
-export const SOURCE_EDITOR_INSTANCE_ERROR_NO_EL = __(
- '"el" parameter is required for createInstance()',
-);
+import { s__ } from '~/locale';
export const URI_PREFIX = 'gitlab';
export const CONTENT_UPDATE_DEBOUNCE = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
-export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
- 'Source Editor instance is required to set up an extension.',
-);
-
export const EDITOR_READY_EVENT = 'editor-ready';
export const EDITOR_TYPE_CODE = 'vs.editor.ICodeEditor';
@@ -20,6 +12,32 @@ export const EDITOR_TYPE_DIFF = 'vs.editor.IDiffEditor';
export const EDITOR_CODE_INSTANCE_FN = 'createInstance';
export const EDITOR_DIFF_INSTANCE_FN = 'createDiffInstance';
+export const SOURCE_EDITOR_INSTANCE_ERROR_NO_EL = s__(
+ 'SourceEditor|"el" parameter is required for createInstance()',
+);
+export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = s__(
+ 'SourceEditor|Source Editor instance is required to set up an extension.',
+);
+export const EDITOR_EXTENSION_DEFINITION_ERROR = s__(
+ 'SourceEditor|Extension definition should be either a class or a function',
+);
+export const EDITOR_EXTENSION_NO_DEFINITION_ERROR = s__(
+ 'SourceEditor|`definition` property is expected on the extension.',
+);
+export const EDITOR_EXTENSION_DEFINITION_TYPE_ERROR = s__(
+ 'SourceEditor|Extension definition should be either class, function, or an Array of definitions.',
+);
+export const EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR = s__(
+ 'SourceEditor|No extension for unuse has been specified.',
+);
+export const EDITOR_EXTENSION_NOT_REGISTERED_ERROR = s__('SourceEditor|%{name} is not registered.');
+export const EDITOR_EXTENSION_NAMING_CONFLICT_ERROR = s__(
+ 'SourceEditor|Name conflict for "%{prop}()" method.',
+);
+export const EDITOR_EXTENSION_STORE_IS_MISSING_ERROR = s__(
+ 'SourceEditor|Extensions Store is required to check for an extension.',
+);
+
//
// EXTENSIONS' CONSTANTS
//
diff --git a/app/assets/javascripts/editor/extensions/example_source_editor_extension.js b/app/assets/javascripts/editor/extensions/example_source_editor_extension.js
new file mode 100644
index 00000000000..119a2aea9eb
--- /dev/null
+++ b/app/assets/javascripts/editor/extensions/example_source_editor_extension.js
@@ -0,0 +1,116 @@
+// THIS IS AN EXAMPLE
+//
+// This file contains a basic documented example of the Source Editor extensions'
+// API for your convenience. You can copy/paste it into your own file
+// and adjust as you see fit
+//
+
+export class MyFancyExtension {
+ /**
+ * THE LIFE-CYCLE CALLBACKS
+ */
+
+ /**
+ * Is called before the extension gets used by an instance,
+ * Use `onSetup` to setup Monaco directly:
+ * actions, keystrokes, update options, etc.
+ * Is called only once before the extension gets registered
+ *
+ * @param { Object } [setupOptions] The setupOptions object
+ * @param { Object } [instance] The Source Editor instance
+ */
+ // eslint-disable-next-line class-methods-use-this,no-unused-vars
+ onSetup(setupOptions, instance) {}
+
+ /**
+ * The first thing called after the extension is
+ * registered and used by an instance.
+ * Is called every time the extension is applied
+ *
+ * @param { Object } [instance] The Source Editor instance
+ */
+ // eslint-disable-next-line class-methods-use-this,no-unused-vars
+ onUse(instance) {}
+
+ /**
+ * Is called before un-using an extension. Can be used for time-critical
+ * actions like cleanup, reverting visual changes, and other user-facing
+ * updates.
+ *
+ * @param { Object } [instance] The Source Editor instance
+ */
+ // eslint-disable-next-line class-methods-use-this,no-unused-vars
+ onBeforeUnuse(instance) {}
+
+ /**
+ * Is called right after an extension is removed from an instance (un-used)
+ * Can be used for non time-critical tasks like cleanup on the Monaco level
+ * (removing actions, keystrokes, etc.).
+ * onUnuse() will be executed during the browser's idle period
+ * (https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)
+ *
+ * @param { Object } [instance] The Source Editor instance
+ */
+ // eslint-disable-next-line class-methods-use-this,no-unused-vars
+ onUnuse(instance) {}
+
+ /**
+ * The public API of the extension: these are the methods that will be exposed
+ * to the end user
+ * @returns {Object}
+ */
+ provides() {
+ return {
+ basic: () => {
+ // The most basic method not depending on anything
+ // Use: instance.basic();
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return 'Foo Bar';
+ },
+ basicWithProp: () => {
+ // The methods with access to the props of the extension.
+ // The props can be either hardcoded (for example in `onSetup`), or
+ // can be dynamically passed as part of `setupOptions` object when
+ // using the extension.
+ // Use: instance.use({ definition: MyFancyExtension, setupOptions: { foo: 'bar' }});
+ return this.foo;
+ },
+ basicWithPropsAsList: (prop1, prop2) => {
+ // Just a simple method with local props
+ // The props are passed as usually.
+ // Use: instance.basicWithPropsAsList(prop1, prop2);
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `The prop1 is ${prop1}; the prop2 is ${prop2}`;
+ },
+ basicWithInstance: (instance) => {
+ // The method accessing the instance methods: either own or provided
+ // by previously-registered extensions
+ // `instance` is always supplied to all methods in provides() as THE LAST
+ // argument.
+ // You don't need to explicitly pass instance to this method:
+ // Use: instance.basicWithInstance();
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `We have access to the whole Instance! ${instance.alpha()}`;
+ },
+ advancedWithInstanceAndProps: ({ author, book } = {}, firstname, lastname, instance) => {
+ // Advanced method where
+ // { author, book } — are the props passed as an object
+ // prop1, prop2 — are the props passed as simple list
+ // instance — is automatically supplied, no need to pass it to
+ // the method explicitly
+ // Use: instance.advancedWithInstanceAndProps(
+ // {
+ // author: 'Franz Kafka',
+ // book: 'The Transformation'
+ // },
+ // 'Franz',
+ // 'Kafka'
+ // );
+ return `
+The author is ${author}; the book is ${book}
+The author's name is ${firstname}; the last name is ${lastname}
+We have access to the whole Instance! For example, 'instance.alpha()': ${instance.alpha()}`;
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
index 5fa01f03f7e..03c68fed3b1 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
@@ -36,12 +36,24 @@ export class SourceEditorExtension {
});
}
- static highlightLines(instance) {
- const { hash } = window.location;
- if (!hash) {
- return;
- }
- const [start, end] = hash.replace(hashRegexp, '').split('-');
+ static removeHighlights(instance) {
+ Object.assign(instance, {
+ lineDecorations: instance.deltaDecorations(instance.lineDecorations || [], []),
+ });
+ }
+
+ /**
+ * Returns a function that can only be invoked once between
+ * each browser screen repaint.
+ * @param {Object} instance - The Source Editor instance
+ * @param {Array} bounds - The [start, end] array with start
+ * and end coordinates for highlighting
+ */
+ static highlightLines(instance, bounds = null) {
+ const [start, end] =
+ bounds && Array.isArray(bounds)
+ ? bounds
+ : window.location.hash?.replace(hashRegexp, '').split('-');
let startLine = start ? parseInt(start, 10) : null;
let endLine = end ? parseInt(end, 10) : startLine;
if (endLine < startLine) {
@@ -51,15 +63,12 @@ export class SourceEditorExtension {
window.requestAnimationFrame(() => {
instance.revealLineInCenter(startLine);
Object.assign(instance, {
- lineDecorations: instance.deltaDecorations(
- [],
- [
- {
- range: new Range(startLine, 1, endLine, 1),
- options: { isWholeLine: true, className: 'active-line-text' },
- },
- ],
- ),
+ lineDecorations: instance.deltaDecorations(instance.lineDecorations || [], [
+ {
+ range: new Range(startLine, 1, endLine, 1),
+ options: { isWholeLine: true, className: 'active-line-text' },
+ },
+ ]),
});
});
}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js b/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js
new file mode 100644
index 00000000000..212e09c8724
--- /dev/null
+++ b/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js
@@ -0,0 +1,293 @@
+import { toPath } from 'lodash';
+import { parseDocument, Document, visit, isScalar, isCollection, isMap } from 'yaml';
+import { findPair } from 'yaml/util';
+import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
+
+export class YamlEditorExtension extends SourceEditorExtension {
+ /**
+ * Extends the source editor with capabilities for yaml files.
+ *
+ * @param { Instance } instance Source Editor Instance
+ * @param { boolean } enableComments Convert model nodes with the comment
+ * pattern to comments?
+ * @param { string } highlightPath Add a line highlight to the
+ * node specified by this e.g. `"foo.bar[0]"`
+ * @param { * } model Any JS Object that will be stringified and used as the
+ * editor's value. Equivalent to using `setDataModel()`
+ * @param options SourceEditorExtension Options
+ */
+ constructor({
+ instance,
+ enableComments = false,
+ highlightPath = null,
+ model = null,
+ ...options
+ } = {}) {
+ super({
+ instance,
+ options: {
+ ...options,
+ enableComments,
+ highlightPath,
+ },
+ });
+
+ if (model) {
+ YamlEditorExtension.initFromModel(instance, model);
+ }
+
+ instance.onDidChangeModelContent(() => instance.onUpdate());
+ }
+
+ /**
+ * @private
+ */
+ static initFromModel(instance, model) {
+ const doc = new Document(model);
+ if (instance.options.enableComments) {
+ YamlEditorExtension.transformComments(doc);
+ }
+ instance.setValue(doc.toString());
+ }
+
+ /**
+ * @private
+ * This wraps long comments to a maximum line length of 80 chars.
+ *
+ * The `yaml` package does not currently wrap comments. This function
+ * is a local workaround and should be deprecated if
+ * https://github.com/eemeli/yaml/issues/322
+ * is resolved.
+ */
+ static wrapCommentString(string, level = 0) {
+ if (!string) {
+ return null;
+ }
+ if (level < 0 || Number.isNaN(parseInt(level, 10))) {
+ throw Error(`Invalid value "${level}" for variable \`level\``);
+ }
+ const maxLineWidth = 80;
+ const indentWidth = 2;
+ const commentMarkerWidth = '# '.length;
+ const maxLength = maxLineWidth - commentMarkerWidth - level * indentWidth;
+ const lines = [[]];
+ string.split(' ').forEach((word) => {
+ const currentLine = lines.length - 1;
+ if ([...lines[currentLine], word].join(' ').length <= maxLength) {
+ lines[currentLine].push(word);
+ } else {
+ lines.push([word]);
+ }
+ });
+ return lines.map((line) => ` ${line.join(' ')}`).join('\n');
+ }
+
+ /**
+ * @private
+ *
+ * This utilizes `yaml`'s `visit` function to transform nodes with a
+ * comment key pattern to actual comments.
+ *
+ * In Objects, a key of '#' will be converted to a comment at the top of a
+ * property. Any key following the pattern `#|<some key>` will be placed
+ * right before `<some key>`.
+ *
+ * In Arrays, any string that starts with # (including the space), will
+ * be converted to a comment at the position it was in.
+ *
+ * @param { Document } doc
+ * @returns { Document }
+ */
+ static transformComments(doc) {
+ const getLevel = (path) => {
+ const { length } = path.filter((x) => isCollection(x));
+ return length ? length - 1 : 0;
+ };
+
+ visit(doc, {
+ Pair(_, pair, path) {
+ const key = pair.key.value;
+ // If the key is = '#', we add the value as a comment to the parent
+ // We can then remove the node.
+ if (key === '#') {
+ Object.assign(path[path.length - 1], {
+ commentBefore: YamlEditorExtension.wrapCommentString(pair.value.value, getLevel(path)),
+ });
+ return visit.REMOVE;
+ }
+ // If the key starts with `#|`, we want to add a comment to the
+ // corresponding property. We can then remove the node.
+ if (key.startsWith('#|')) {
+ const targetProperty = key.split('|')[1];
+ const target = findPair(path[path.length - 1].items, targetProperty);
+ if (target) {
+ target.key.commentBefore = YamlEditorExtension.wrapCommentString(
+ pair.value.value,
+ getLevel(path),
+ );
+ }
+ return visit.REMOVE;
+ }
+ return undefined; // If the node is not a comment, do nothing with it
+ },
+ // Sequence is basically an array
+ Seq(_, node, path) {
+ let comment = null;
+ const items = node.items.flatMap((child) => {
+ if (comment) {
+ Object.assign(child, { commentBefore: comment });
+ comment = null;
+ }
+ if (
+ isScalar(child) &&
+ child.value &&
+ child.value.startsWith &&
+ child.value.startsWith('#')
+ ) {
+ const commentValue = child.value.replace(/^#\s?/, '');
+ comment = YamlEditorExtension.wrapCommentString(commentValue, getLevel(path));
+ return [];
+ }
+ return child;
+ });
+ Object.assign(node, { items });
+ // Adding a comment in case the last one is a comment
+ if (comment) {
+ Object.assign(node, { comment });
+ }
+ },
+ });
+ return doc;
+ }
+
+ /**
+ * Get the editor's value parsed as a `Document` as defined by the `yaml`
+ * package
+ * @returns {Document}
+ */
+ getDoc() {
+ return parseDocument(this.getValue());
+ }
+
+ /**
+ * Accepts a `Document` as defined by the `yaml` package and
+ * sets the Editor's value to a stringified version of it.
+ * @param { Document } doc
+ */
+ setDoc(doc) {
+ if (this.options.enableComments) {
+ YamlEditorExtension.transformComments(doc);
+ }
+
+ if (!this.getValue()) {
+ this.setValue(doc.toString());
+ } else {
+ this.updateValue(doc.toString());
+ }
+ }
+
+ /**
+ * Returns the parsed value of the Editor's content as JS.
+ * @returns {*}
+ */
+ getDataModel() {
+ return this.getDoc().toJS();
+ }
+
+ /**
+ * Accepts any JS Object and sets the Editor's value to a stringified version
+ * of that value.
+ *
+ * @param value
+ */
+ setDataModel(value) {
+ this.setDoc(new Document(value));
+ }
+
+ /**
+ * Method to be executed when the Editor's <TextModel> was updated
+ */
+ onUpdate() {
+ if (this.options.highlightPath) {
+ this.highlight(this.options.highlightPath);
+ }
+ }
+
+ /**
+ * Set the editors content to the input without recreating the content model.
+ *
+ * @param blob
+ */
+ updateValue(blob) {
+ // Using applyEdits() instead of setValue() ensures that tokens such as
+ // highlighted lines aren't deleted/recreated which causes a flicker.
+ const model = this.getModel();
+ model.applyEdits([
+ {
+ // A nice improvement would be to replace getFullModelRange() with
+ // a range of the actual diff, avoiding re-formatting the document,
+ // but that's something for a later iteration.
+ range: model.getFullModelRange(),
+ text: blob,
+ },
+ ]);
+ }
+
+ /**
+ * Add a line highlight style to the node specified by the path.
+ *
+ * @param {string|null|false} path A path to a node of the Editor's value,
+ * e.g. `"foo.bar[0]"`. If the value is falsy, this will remove all
+ * highlights.
+ */
+ highlight(path) {
+ if (this.options.highlightPath === path) return;
+ if (!path) {
+ SourceEditorExtension.removeHighlights(this);
+ } else {
+ const res = this.locate(path);
+ SourceEditorExtension.highlightLines(this, res);
+ }
+ this.options.highlightPath = path || null;
+ }
+
+ /**
+ * Return the line numbers of a certain node identified by `path` within
+ * the yaml.
+ *
+ * @param {string} path A path to a node, eg. `foo.bar[0]`
+ * @returns {number[]} Array following the schema `[firstLine, lastLine]`
+ * (both inclusive)
+ *
+ * @throws {Error} Will throw if the path is not found inside the document
+ */
+ locate(path) {
+ if (!path) throw Error(`No path provided.`);
+ const blob = this.getValue();
+ const doc = parseDocument(blob);
+ const pathArray = toPath(path);
+
+ if (!doc.getIn(pathArray)) {
+ throw Error(`The node ${path} could not be found inside the document.`);
+ }
+
+ const parentNode = doc.getIn(pathArray.slice(0, pathArray.length - 1));
+ let startChar;
+ let endChar;
+ if (isMap(parentNode)) {
+ const node = parentNode.items.find(
+ (item) => item.key.value === pathArray[pathArray.length - 1],
+ );
+ [startChar] = node.key.range;
+ [, , endChar] = node.value.range;
+ } else {
+ const node = doc.getIn(pathArray);
+ [startChar, , endChar] = node.range;
+ }
+ const startSlice = blob.slice(0, startChar);
+ const endSlice = blob.slice(0, endChar);
+ const startLine = (startSlice.match(/\n/g) || []).length + 1;
+ const endLine = (endSlice.match(/\n/g) || []).length;
+ return [startLine, endLine];
+ }
+}
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 0052bc00406..f0db3e5594b 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -63,9 +63,9 @@
"items": {
"type": "object",
"properties": {
- "if": {
- "type": "string"
- },
+ "if": { "$ref": "#/definitions/if" },
+ "changes": { "$ref": "#/definitions/changes" },
+ "exists": { "$ref": "#/definitions/exists" },
"variables": { "$ref": "#/definitions/variables" },
"when": {
"type": "string",
@@ -497,24 +497,9 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "if": {
- "type": "string",
- "description": "Expression to evaluate whether additional attributes should be provided to the job"
- },
- "changes": {
- "type": "array",
- "description": "Additional attributes will be provided to job if any of the provided paths matches a modified file",
- "items": {
- "type": "string"
- }
- },
- "exists": {
- "type": "array",
- "description": "Additional attributes will be provided to job if any of the provided paths matches an existing file in the repository",
- "items": {
- "type": "string"
- }
- },
+ "if": { "$ref": "#/definitions/if" },
+ "changes": { "$ref": "#/definitions/changes" },
+ "exists": { "$ref": "#/definitions/exists" },
"variables": { "$ref": "#/definitions/variables" },
"when": { "$ref": "#/definitions/when" },
"start_in": { "$ref": "#/definitions/start_in" },
@@ -541,6 +526,24 @@
]
}
},
+ "if": {
+ "type": "string",
+ "description": "Expression to evaluate whether additional attributes should be provided to the job"
+ },
+ "changes": {
+ "type": "array",
+ "description": "Additional attributes will be provided to job if any of the provided paths matches a modified file",
+ "items": {
+ "type": "string"
+ }
+ },
+ "exists": {
+ "type": "array",
+ "description": "Additional attributes will be provided to job if any of the provided paths matches an existing file in the repository",
+ "items": {
+ "type": "string"
+ }
+ },
"variables": {
"type": "object",
"description": "Defines environment variables for specific jobs. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off.",
@@ -555,7 +558,7 @@
},
"start_in": {
"type": "string",
- "description": "Used in conjunction with 'when: delayed' to set how long to delay before starting a job.",
+ "description": "Used in conjunction with 'when: delayed' to set how long to delay before starting a job. e.g. '5', 5 seconds, 30 minutes, 1 week, etc. Read more: https://docs.gitlab.com/ee/ci/jobs/job_control.html#run-a-job-after-a-delay",
"minLength": 1
},
"allow_failure": {
@@ -939,7 +942,7 @@
"stage": {
"type": "string",
"description": "Define what stage the job will run in.",
- "default": "test"
+ "minLength": 1
},
"only": {
"$ref": "#/definitions/filter",
diff --git a/app/assets/javascripts/editor/source_editor_extension.js b/app/assets/javascripts/editor/source_editor_extension.js
new file mode 100644
index 00000000000..f6bc62a1c09
--- /dev/null
+++ b/app/assets/javascripts/editor/source_editor_extension.js
@@ -0,0 +1,17 @@
+import { EDITOR_EXTENSION_DEFINITION_ERROR } from './constants';
+
+export default class EditorExtension {
+ constructor({ definition, setupOptions } = {}) {
+ if (typeof definition !== 'function') {
+ throw new Error(EDITOR_EXTENSION_DEFINITION_ERROR);
+ }
+ this.name = definition.name; // both class- and fn-based extensions have a name
+ this.setupOptions = setupOptions;
+ // eslint-disable-next-line new-cap
+ this.obj = new definition();
+ }
+
+ get api() {
+ return this.obj.provides?.();
+ }
+}
diff --git a/app/assets/javascripts/editor/source_editor_instance.js b/app/assets/javascripts/editor/source_editor_instance.js
new file mode 100644
index 00000000000..e0ca4ea518b
--- /dev/null
+++ b/app/assets/javascripts/editor/source_editor_instance.js
@@ -0,0 +1,271 @@
+/**
+ * @module source_editor_instance
+ */
+
+/**
+ * A Source Editor Extension definition
+ * @typedef {Object} SourceEditorExtensionDefinition
+ * @property {Object} definition
+ * @property {Object} setupOptions
+ */
+
+/**
+ * A Source Editor Extension
+ * @typedef {Object} SourceEditorExtension
+ * @property {Object} obj
+ * @property {string} name
+ * @property {Object} api
+ */
+
+import { isEqual } from 'lodash';
+import { editor as monacoEditor } from 'monaco-editor';
+import { getBlobLanguage } from '~/editor/utils';
+import { logError } from '~/lib/logger';
+import { sprintf } from '~/locale';
+import EditorExtension from './source_editor_extension';
+import {
+ EDITOR_EXTENSION_DEFINITION_TYPE_ERROR,
+ EDITOR_EXTENSION_NAMING_CONFLICT_ERROR,
+ EDITOR_EXTENSION_NO_DEFINITION_ERROR,
+ EDITOR_EXTENSION_NOT_REGISTERED_ERROR,
+ EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR,
+ EDITOR_EXTENSION_STORE_IS_MISSING_ERROR,
+} from './constants';
+
+const utils = {
+ removeExtFromMethod: (method, extensionName, container) => {
+ if (!container) {
+ return;
+ }
+ if (Object.prototype.hasOwnProperty.call(container, method)) {
+ // eslint-disable-next-line no-param-reassign
+ delete container[method];
+ }
+ },
+
+ getStoredExtension: (extensionsStore, name) => {
+ if (!extensionsStore) {
+ logError(EDITOR_EXTENSION_STORE_IS_MISSING_ERROR);
+ return undefined;
+ }
+ return extensionsStore.get(name);
+ },
+};
+
+/** Class representing a Source Editor Instance */
+export default class EditorInstance {
+ /**
+ * Create a Source Editor Instance
+ * @param {Object} rootInstance - Monaco instance to build on top of
+ * @param {Map} extensionsStore - The global registry for the extension instances
+ * @returns {Object} - A Proxy returning props/methods from either registered extensions, or Source Editor instance, or underlying Monaco instance
+ */
+ constructor(rootInstance = {}, extensionsStore = new Map()) {
+ /** The methods provided by extensions. */
+ this.methods = {};
+
+ const seInstance = this;
+ const getHandler = {
+ get(target, prop, receiver) {
+ const methodExtension =
+ Object.prototype.hasOwnProperty.call(seInstance.methods, prop) &&
+ seInstance.methods[prop];
+ if (methodExtension) {
+ const extension = extensionsStore.get(methodExtension);
+
+ return (...args) => {
+ return extension.api[prop].call(seInstance, ...args, receiver);
+ };
+ }
+ return Reflect.get(seInstance[prop] ? seInstance : target, prop, receiver);
+ },
+ set(target, prop, value) {
+ Object.assign(seInstance, {
+ [prop]: value,
+ });
+ return true;
+ },
+ };
+ const instProxy = new Proxy(rootInstance, getHandler);
+
+ /**
+ * Main entry point to apply an extension to the instance
+ * @param {SourceEditorExtensionDefinition}
+ */
+ this.use = EditorInstance.useUnuse.bind(instProxy, extensionsStore, this.useExtension);
+
+ /**
+ * Main entry point to un-use an extension and remove it from the instance
+ * @param {SourceEditorExtension}
+ */
+ this.unuse = EditorInstance.useUnuse.bind(instProxy, extensionsStore, this.unuseExtension);
+
+ return instProxy;
+ }
+
+ /**
+ * A private dispatcher function for both `use` and `unuse`
+ * @param {Map} extensionsStore - The global registry for the extension instances
+ * @param {Function} fn - A function to route to. Either `this.useExtension` or `this.unuseExtension`
+ * @param {SourceEditorExtensionDefinition[]} extensions - The extensions to use/unuse.
+ * @returns {Function}
+ */
+ static useUnuse(extensionsStore, fn, extensions) {
+ if (Array.isArray(extensions)) {
+ /**
+ * We cut short if the Array is empty and let the destination function to throw
+ * Otherwise, we run the destination function on every entry of the Array
+ */
+ return extensions.length
+ ? extensions.map(fn.bind(this, extensionsStore))
+ : fn.call(this, extensionsStore);
+ }
+ return fn.call(this, extensionsStore, extensions);
+ }
+
+ //
+ // REGISTERING NEW EXTENSION
+ //
+
+ /**
+ * Run all registrations when using an extension
+ * @param {Map} extensionsStore - The global registry for the extension instances
+ * @param {SourceEditorExtensionDefinition} extension - The extension definition to use.
+ * @returns {EditorExtension|*}
+ */
+ useExtension(extensionsStore, extension = {}) {
+ const { definition } = extension;
+ if (!definition) {
+ throw new Error(EDITOR_EXTENSION_NO_DEFINITION_ERROR);
+ }
+ if (typeof definition !== 'function') {
+ throw new Error(EDITOR_EXTENSION_DEFINITION_TYPE_ERROR);
+ }
+
+ // Existing Extension Path
+ const existingExt = utils.getStoredExtension(extensionsStore, definition.name);
+ if (existingExt) {
+ if (isEqual(extension.setupOptions, existingExt.setupOptions)) {
+ return existingExt;
+ }
+ this.unuseExtension(extensionsStore, existingExt);
+ }
+
+ // New Extension Path
+ const extensionInstance = new EditorExtension(extension);
+ const { setupOptions, obj: extensionObj } = extensionInstance;
+ if (extensionObj.onSetup) {
+ extensionObj.onSetup(setupOptions, this);
+ }
+ if (extensionsStore) {
+ this.registerExtension(extensionInstance, extensionsStore);
+ }
+ this.registerExtensionMethods(extensionInstance);
+ return extensionInstance;
+ }
+
+ /**
+ * Register extension in the global extensions store
+ * @param {SourceEditorExtension} extension - Instance of Source Editor extension
+ * @param {Map} extensionsStore - The global registry for the extension instances
+ */
+ registerExtension(extension, extensionsStore) {
+ const { name } = extension;
+ const hasExtensionRegistered =
+ extensionsStore.has(name) &&
+ isEqual(extension.setupOptions, extensionsStore.get(name).setupOptions);
+ if (hasExtensionRegistered) {
+ return;
+ }
+ extensionsStore.set(name, extension);
+ const { obj: extensionObj } = extension;
+ if (extensionObj.onUse) {
+ extensionObj.onUse(this);
+ }
+ }
+
+ /**
+ * Register extension methods in the registry on the instance
+ * @param {SourceEditorExtension} extension - Instance of Source Editor extension
+ */
+ registerExtensionMethods(extension) {
+ const { api, name } = extension;
+
+ if (!api) {
+ return;
+ }
+
+ Object.keys(api).forEach((prop) => {
+ if (this[prop]) {
+ logError(sprintf(EDITOR_EXTENSION_NAMING_CONFLICT_ERROR, { prop }));
+ } else {
+ this.methods[prop] = name;
+ }
+ }, this);
+ }
+
+ //
+ // UNREGISTERING AN EXTENSION
+ //
+
+ /**
+ * Unregister extension with the cleanup
+ * @param {Map} extensionsStore - The global registry for the extension instances
+ * @param {SourceEditorExtension} extension - Instance of Source Editor extension to un-use
+ */
+ unuseExtension(extensionsStore, extension) {
+ if (!extension) {
+ throw new Error(EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR);
+ }
+ const { name } = extension;
+ const existingExt = utils.getStoredExtension(extensionsStore, name);
+ if (!existingExt) {
+ throw new Error(sprintf(EDITOR_EXTENSION_NOT_REGISTERED_ERROR, { name }));
+ }
+ const { obj: extensionObj } = existingExt;
+ if (extensionObj.onBeforeUnuse) {
+ extensionObj.onBeforeUnuse(this);
+ }
+ this.unregisterExtensionMethods(existingExt);
+ if (extensionObj.onUnuse) {
+ extensionObj.onUnuse(this);
+ }
+ }
+
+ /**
+ * Remove all methods associated with this extension from the registry on the instance
+ * @param {SourceEditorExtension} extension - Instance of Source Editor extension to un-use
+ */
+ unregisterExtensionMethods(extension) {
+ const { api, name } = extension;
+ if (!api) {
+ return;
+ }
+ Object.keys(api).forEach((method) => {
+ utils.removeExtFromMethod(method, name, this.methods);
+ });
+ }
+
+ /**
+ * PUBLIC API OF AN INSTANCE
+ */
+
+ /**
+ * Updates model language based on the path
+ * @param {String} path - blob path
+ */
+ updateModelLanguage(path) {
+ const lang = getBlobLanguage(path);
+ const model = this.getModel();
+ // return monacoEditor.setModelLanguage(model, lang);
+ monacoEditor.setModelLanguage(model, lang);
+ }
+
+ /**
+ * Get the methods returned by extensions.
+ * @returns {Array}
+ */
+ get extensionsAPI() {
+ return Object.keys(this.methods);
+ }
+}
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index 7672151af2a..478e3f6aed9 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -9,6 +9,7 @@ let emojiMap = null;
let validEmojiNames = null;
export const FALLBACK_EMOJI_KEY = 'grey_question';
+// Keep the version in sync with `lib/gitlab/emoji.rb`
export const EMOJI_VERSION = '1';
const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue
index 2eb2be351b3..26ec882472b 100644
--- a/app/assets/javascripts/environments/components/delete_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlModal } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
@@ -27,7 +27,7 @@ export default {
},
cancelProps() {
return {
- text: s__('Cancel'),
+ text: __('Cancel'),
};
},
confirmDeleteMessage() {
diff --git a/app/assets/javascripts/environments/components/new_environment_folder.vue b/app/assets/javascripts/environments/components/new_environment_folder.vue
new file mode 100644
index 00000000000..0615bdef537
--- /dev/null
+++ b/app/assets/javascripts/environments/components/new_environment_folder.vue
@@ -0,0 +1,69 @@
+<script>
+import { GlCollapse, GlIcon, GlBadge, GlLink } from '@gitlab/ui';
+import folderQuery from '../graphql/queries/folder.query.graphql';
+
+export default {
+ components: {
+ GlCollapse,
+ GlIcon,
+ GlBadge,
+ GlLink,
+ },
+ props: {
+ nestedEnvironment: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return { visible: false };
+ },
+ apollo: {
+ folder: {
+ query: folderQuery,
+ variables() {
+ return { environment: this.nestedEnvironment.latest };
+ },
+ },
+ },
+ computed: {
+ icons() {
+ return this.visible
+ ? { caret: 'angle-down', folder: 'folder-open' }
+ : { caret: 'angle-right', folder: 'folder-o' };
+ },
+ count() {
+ return this.folder?.availableCount ?? 0;
+ },
+ folderClass() {
+ return { 'gl-font-weight-bold': this.visible };
+ },
+ folderPath() {
+ return this.nestedEnvironment.latest.folderPath;
+ },
+ },
+ methods: {
+ toggleCollapse() {
+ this.visible = !this.visible;
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-border-b-solid gl-border-gray-100 gl-border-1 gl-px-3 gl-pt-3 gl-pb-5">
+ <div class="gl-w-full gl-display-flex gl-align-items-center" @click="toggleCollapse">
+ <gl-icon
+ class="gl-mr-2 gl-fill-current-color gl-text-gray-500"
+ :name="icons.caret"
+ :size="12"
+ />
+ <gl-icon class="gl-mr-2 gl-fill-current-color gl-text-gray-500" :name="icons.folder" />
+ <div class="gl-mr-2 gl-text-gray-500" :class="folderClass">
+ {{ nestedEnvironment.name }}
+ </div>
+ <gl-badge size="sm" class="gl-mr-auto">{{ count }}</gl-badge>
+ <gl-link v-if="visible" :href="folderPath">{{ s__('Environments|Show all') }}</gl-link>
+ </div>
+ <gl-collapse :visible="visible" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/new_environments_app.vue b/app/assets/javascripts/environments/components/new_environments_app.vue
new file mode 100644
index 00000000000..a5526f9cd71
--- /dev/null
+++ b/app/assets/javascripts/environments/components/new_environments_app.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlBadge, GlTab, GlTabs } from '@gitlab/ui';
+import environmentAppQuery from '../graphql/queries/environmentApp.query.graphql';
+import EnvironmentFolder from './new_environment_folder.vue';
+
+export default {
+ components: {
+ EnvironmentFolder,
+ GlBadge,
+ GlTab,
+ GlTabs,
+ },
+ apollo: {
+ environmentApp: {
+ query: environmentAppQuery,
+ },
+ },
+ computed: {
+ folders() {
+ return this.environmentApp?.environments.filter((e) => e.size > 1) ?? [];
+ },
+ availableCount() {
+ return this.environmentApp?.availableCount;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-tabs>
+ <gl-tab>
+ <template #title>
+ <span>{{ __('Available') }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge">
+ {{ availableCount }}
+ </gl-badge>
+ </template>
+ <environment-folder
+ v-for="folder in folders"
+ :key="folder.name"
+ class="gl-mb-3"
+ :nested-environment="folder"
+ />
+ </gl-tab>
+ </gl-tabs>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/rollback_modal_manager.vue b/app/assets/javascripts/environments/components/rollback_modal_manager.vue
index 6aa7d96fdfd..3a8b9ebcb84 100644
--- a/app/assets/javascripts/environments/components/rollback_modal_manager.vue
+++ b/app/assets/javascripts/environments/components/rollback_modal_manager.vue
@@ -15,7 +15,6 @@ export default {
data() {
return {
environment: null,
- retryPath: '',
visible: false,
};
},
@@ -35,9 +34,9 @@ export default {
name: environmentName,
commitShortSha,
commitUrl,
+ retryUrl: retryPath,
isLastDeployment: parseBoolean(isLastDeployment),
};
- this.retryPath = retryPath;
this.visible = true;
});
});
@@ -51,7 +50,5 @@ export default {
v-model="visible"
:environment="environment"
:has-multiple-commits="false"
- :retry-url="retryPath"
/>
- <div v-else></div>
</template>
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index f248e9ec079..206381e0b7e 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -8,7 +8,7 @@ Vue.use(Translate);
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
export default () => {
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
new file mode 100644
index 00000000000..c734c2fba0c
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -0,0 +1,25 @@
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import environmentApp from './queries/environmentApp.query.graphql';
+import { resolvers } from './resolvers';
+import typeDefs from './typedefs.graphql';
+
+export const apolloProvider = (endpoint) => {
+ const defaultClient = createDefaultClient(resolvers(endpoint), {
+ typeDefs,
+ });
+ const { cache } = defaultClient;
+
+ cache.writeQuery({
+ query: environmentApp,
+ data: {
+ availableCount: 0,
+ environments: [],
+ reviewApp: {},
+ stoppedCount: 0,
+ },
+ });
+ return new VueApollo({
+ defaultClient,
+ });
+};
diff --git a/app/assets/javascripts/environments/graphql/mutations/cancel_auto_stop.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/cancel_auto_stop.mutation.graphql
new file mode 100644
index 00000000000..22dfb8a7a89
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/mutations/cancel_auto_stop.mutation.graphql
@@ -0,0 +1,5 @@
+mutation cancelAutoStop($environment: LocalEnvironment) {
+ cancelAutoStop(environment: $environment) @client {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/mutations/delete_environment.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/delete_environment.mutation.graphql
new file mode 100644
index 00000000000..9bb68857923
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/mutations/delete_environment.mutation.graphql
@@ -0,0 +1,5 @@
+mutation deleteEnvironment($environment: LocalEnvironment) {
+ deleteEnvironment(environment: $environment) @client {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/mutations/rollback_environment.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/rollback_environment.mutation.graphql
new file mode 100644
index 00000000000..3db4dc2b9a5
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/mutations/rollback_environment.mutation.graphql
@@ -0,0 +1,5 @@
+mutation rollbackEnvironment($environment: LocalEnvironment) {
+ rollbackEnvironment(environment: $environment) @client {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/mutations/stop_environment.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/stop_environment.mutation.graphql
new file mode 100644
index 00000000000..7eae0ef4ce4
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/mutations/stop_environment.mutation.graphql
@@ -0,0 +1,5 @@
+mutation stopEnvironment($environment: LocalEnvironment) {
+ stopEnvironment(environment: $environment) @client {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql
index 04ea5cbcaef..936bf49a1ac 100644
--- a/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql
+++ b/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql
@@ -1,4 +1,4 @@
-mutation($input: EnvironmentsCanaryIngressUpdateInput!) {
+mutation updateCanaryIngress($input: EnvironmentsCanaryIngressUpdateInput!) {
environmentsCanaryIngressUpdate(input: $input) {
errors
}
diff --git a/app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql b/app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql
new file mode 100644
index 00000000000..faa76c0a42c
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql
@@ -0,0 +1,8 @@
+query getEnvironmentApp {
+ environmentApp @client {
+ availableCount
+ environments
+ reviewApp
+ stoppedCount
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
new file mode 100644
index 00000000000..3292c916b2e
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
@@ -0,0 +1,7 @@
+query getEnvironmentFolder($environment: NestedLocalEnvironment) {
+ folder(environment: $environment) @client {
+ availableCount
+ environments
+ stoppedCount
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
new file mode 100644
index 00000000000..8322b806370
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -0,0 +1,50 @@
+import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+const mapNestedEnvironment = (env) => ({
+ ...convertObjectPropsToCamelCase(env, { deep: true }),
+ __typename: 'NestedLocalEnvironment',
+});
+const mapEnvironment = (env) => ({
+ ...convertObjectPropsToCamelCase(env),
+ __typename: 'LocalEnvironment',
+});
+
+export const resolvers = (endpoint) => ({
+ Query: {
+ environmentApp() {
+ return axios.get(endpoint, { params: { nested: true } }).then((res) => ({
+ availableCount: res.data.available_count,
+ environments: res.data.environments.map(mapNestedEnvironment),
+ reviewApp: {
+ ...convertObjectPropsToCamelCase(res.data.review_app),
+ __typename: 'ReviewApp',
+ },
+ stoppedCount: res.data.stopped_count,
+ __typename: 'LocalEnvironmentApp',
+ }));
+ },
+ folder(_, { environment: { folderPath } }) {
+ return axios.get(folderPath, { params: { per_page: 3 } }).then((res) => ({
+ availableCount: res.data.available_count,
+ environments: res.data.environments.map(mapEnvironment),
+ stoppedCount: res.data.stopped_count,
+ __typename: 'LocalEnvironmentFolder',
+ }));
+ },
+ },
+ Mutations: {
+ stopEnvironment(_, { environment: { stopPath } }) {
+ return axios.post(stopPath);
+ },
+ deleteEnvironment(_, { environment: { deletePath } }) {
+ return axios.delete(deletePath);
+ },
+ rollbackEnvironment(_, { environment: { retryUrl } }) {
+ return axios.post(retryUrl);
+ },
+ cancelAutoStop(_, { environment: { autoStopPath } }) {
+ return axios.post(autoStopPath);
+ },
+ },
+});
diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql
new file mode 100644
index 00000000000..49ea719449e
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/typedefs.graphql
@@ -0,0 +1,35 @@
+type LocalEnvironment {
+ id: Int!
+ globalId: ID!
+ name: String!
+ folderPath: String
+ stopPath: String
+ deletePath: String
+ retryUrl: String
+ autoStopPath: String
+}
+
+type NestedLocalEnvironment {
+ name: String!
+ size: Int!
+ latest: LocalEnvironment!
+}
+
+type LocalEnvironmentFolder {
+ environments: [LocalEnvironment!]!
+ availableCount: Int!
+ stoppedCount: Int!
+}
+
+type ReviewApp {
+ canSetupReviewApp: Boolean!
+ allClustersEmpty: Boolean!
+ reviewSnippet: String
+}
+
+type LocalEnvironmentApp {
+ stoppedCount: Int!
+ availableCount: Int!
+ environments: [NestedLocalEnvironment!]!
+ reviewApp: ReviewApp!
+}
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index 5e33923d518..3b1d35c1f22 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -9,40 +9,43 @@ Vue.use(Translate);
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
-export default () => {
- const el = document.getElementById('environments-list-view');
- return new Vue({
- el,
- components: {
- environmentsComponent,
- },
- apolloProvider,
- provide: {
- projectPath: el.dataset.projectPath,
- defaultBranchName: el.dataset.defaultBranchName,
- },
- data() {
- const environmentsData = el.dataset;
+export default (el) => {
+ if (el) {
+ return new Vue({
+ el,
+ components: {
+ environmentsComponent,
+ },
+ apolloProvider,
+ provide: {
+ projectPath: el.dataset.projectPath,
+ defaultBranchName: el.dataset.defaultBranchName,
+ },
+ data() {
+ const environmentsData = el.dataset;
- return {
- endpoint: environmentsData.environmentsDataEndpoint,
- newEnvironmentPath: environmentsData.newEnvironmentPath,
- helpPagePath: environmentsData.helpPagePath,
- canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
- };
- },
- render(createElement) {
- return createElement('environments-component', {
- props: {
- endpoint: this.endpoint,
- newEnvironmentPath: this.newEnvironmentPath,
- helpPagePath: this.helpPagePath,
- canCreateEnvironment: this.canCreateEnvironment,
- },
- });
- },
- });
+ return {
+ endpoint: environmentsData.environmentsDataEndpoint,
+ newEnvironmentPath: environmentsData.newEnvironmentPath,
+ helpPagePath: environmentsData.helpPagePath,
+ canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
+ };
+ },
+ render(createElement) {
+ return createElement('environments-component', {
+ props: {
+ endpoint: this.endpoint,
+ newEnvironmentPath: this.newEnvironmentPath,
+ helpPagePath: this.helpPagePath,
+ canCreateEnvironment: this.canCreateEnvironment,
+ },
+ });
+ },
+ });
+ }
+
+ return null;
};
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 85cff73cc3e..0f9741784d6 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -6,7 +6,7 @@ import Visibility from 'visibilityjs';
import createFlash from '~/flash';
import Poll from '../../lib/utils/poll';
import { getParameterByName } from '../../lib/utils/url_utility';
-import { s__ } from '../../locale';
+import { s__, __ } from '../../locale';
import tabs from '../../vue_shared/components/navigation_tabs.vue';
import tablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
import container from '../components/container.vue';
@@ -207,13 +207,13 @@ export default {
tabs() {
return [
{
- name: s__('Available'),
+ name: __('Available'),
scope: 'available',
count: this.state.availableCounter,
isActive: this.scope === 'available',
},
{
- name: s__('Stopped'),
+ name: __('Stopped'),
scope: 'stopped',
count: this.state.stoppedCounter,
isActive: this.scope === 'stopped',
diff --git a/app/assets/javascripts/environments/new_index.js b/app/assets/javascripts/environments/new_index.js
new file mode 100644
index 00000000000..dd5c709c75a
--- /dev/null
+++ b/app/assets/javascripts/environments/new_index.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { parseBoolean } from '../lib/utils/common_utils';
+import { apolloProvider } from './graphql/client';
+import EnvironmentsApp from './components/new_environments_app.vue';
+
+Vue.use(VueApollo);
+
+export default (el) => {
+ if (el) {
+ const {
+ canCreateEnvironment,
+ endpoint,
+ newEnvironmentPath,
+ helpPagePath,
+ projectPath,
+ defaultBranchName,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider: apolloProvider(endpoint),
+ provide: {
+ projectPath,
+ defaultBranchName,
+ endpoint,
+ newEnvironmentPath,
+ helpPagePath,
+ canCreateEnvironment: parseBoolean(canCreateEnvironment),
+ },
+ render(h) {
+ return h(EnvironmentsApp);
+ },
+ });
+ }
+
+ return null;
+};
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index 2b8a31da50f..34d01f21da2 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltip, GlSprintf, GlIcon } from '@gitlab/ui';
+import { GlTooltip, GlSprintf, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -12,6 +12,7 @@ export default {
},
directives: {
GlTooltip,
+ SafeHtml,
},
props: {
lines: {
@@ -129,9 +130,9 @@ export default {
{{ lineNum(line) }}
</td>
<td
+ v-safe-html="lineCode(line)"
class="line_content"
:class="{ old: isHighlighted(lineNum(line)) }"
- v-html="lineCode(line) /* eslint-disable-line vue/no-v-html */"
></td>
</tr>
</template>
diff --git a/app/assets/javascripts/experimentation/utils.js b/app/assets/javascripts/experimentation/utils.js
index 624a04fd7c2..dcb6a8e20a3 100644
--- a/app/assets/javascripts/experimentation/utils.js
+++ b/app/assets/javascripts/experimentation/utils.js
@@ -3,11 +3,24 @@ import { get } from 'lodash';
import { DEFAULT_VARIANT, CANDIDATE_VARIANT, TRACKING_CONTEXT_SCHEMA } from './constants';
function getExperimentsData() {
- return get(window, ['gon', 'experiment'], {});
+ // Pull from deprecated window.gon.experiment
+ const experimentsFromGon = get(window, ['gon', 'experiment'], {});
+ // Pull from preferred window.gl.experiments
+ const experimentsFromGl = get(window, ['gl', 'experiments'], {});
+
+ return { ...experimentsFromGon, ...experimentsFromGl };
}
function convertExperimentDataToExperimentContext(experimentData) {
- return { schema: TRACKING_CONTEXT_SCHEMA, data: experimentData };
+ // Bandaid to allow-list only the properties which the current gitlab_experiment context schema suppports.
+ // See TRACKING_CONTEXT_SCHEMA for current version (1-0-0)
+ // https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-0
+ const { experiment: experimentName, key, variant, migration_keys } = experimentData;
+
+ return {
+ schema: TRACKING_CONTEXT_SCHEMA,
+ data: { experiment: experimentName, key, variant, migration_keys },
+ };
}
export function getExperimentData(experimentName) {
@@ -26,14 +39,14 @@ export function getExperimentVariant(experimentName) {
return getExperimentData(experimentName)?.variant || DEFAULT_VARIANT;
}
-export function experiment(experimentName, variants) {
+export function experiment(experimentName, { use, control, candidate, ...variants }) {
const variant = getExperimentVariant(experimentName);
switch (variant) {
case DEFAULT_VARIANT:
- return variants.use.call();
+ return (use || control).call();
case CANDIDATE_VARIANT:
- return variants.try.call();
+ return (variants.try || candidate).call();
default:
return variants[variant].call();
}
diff --git a/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue b/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue
index d86e13ce722..366ee6bb05b 100644
--- a/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue
+++ b/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue
@@ -213,6 +213,7 @@ export default {
<div
v-if="hasRotateError"
class="gl-text-red-500 gl-display-flex gl-align-items-center gl-font-weight-normal gl-mb-3"
+ data-testid="rotate-error"
>
<gl-icon name="warning" class="gl-mr-2" />
<span>{{ $options.translations.instanceIdRegenerateError }}</span>
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 741171b185a..1287a7ed746 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -9,6 +9,8 @@ const FLASH_TYPES = {
WARNING: 'warning',
};
+const FLASH_CLOSED_EVENT = 'flashClosed';
+
const getCloseEl = (flashEl) => {
return flashEl.querySelector('.js-close-icon');
};
@@ -26,6 +28,7 @@ const hideFlash = (flashEl, fadeTransition = true) => {
() => {
flashEl.remove();
window.dispatchEvent(new Event('resize'));
+ flashEl.dispatchEvent(new Event(FLASH_CLOSED_EVENT));
if (document.body.classList.contains('flash-shown'))
document.body.classList.remove('flash-shown');
},
@@ -132,4 +135,5 @@ export {
hideFlash,
removeFlashClickListener,
FLASH_TYPES,
+ FLASH_CLOSED_EVENT,
};
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 5dac315d345..1700437aa84 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/require-default-prop */
import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
@@ -23,6 +22,7 @@ export default {
matcher: {
type: String,
required: false,
+ default: '',
},
itemId: {
type: Number,
@@ -35,6 +35,7 @@ export default {
namespace: {
type: String,
required: false,
+ default: '',
},
webUrl: {
type: String,
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index cb63c86a4fa..69331ff1a06 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import '~/lib/utils/jquery_at_who';
-import { escape as lodashEscape, sortBy, template } from 'lodash';
+import { escape as lodashEscape, sortBy, template, escapeRegExp } from 'lodash';
import * as Emoji from '~/emoji';
import axios from '~/lib/utils/axios_utils';
import { s__, __, sprintf } from '~/locale';
@@ -65,6 +65,17 @@ export function membersBeforeSave(members) {
});
}
+export const highlighter = (li, query) => {
+ // override default behaviour to escape dot character
+ // see https://github.com/ichord/At.js/pull/576
+ if (!query) {
+ return li;
+ }
+ const escapedQuery = escapeRegExp(query);
+ const regexp = new RegExp(`>\\s*([^<]*?)(${escapedQuery})([^<]*)\\s*<`, 'ig');
+ return li.replace(regexp, (str, $1, $2, $3) => `> ${$1}<strong>${$2}</strong>${$3} <`);
+};
+
export const defaultAutocompleteConfig = {
emojis: true,
members: true,
@@ -664,16 +675,7 @@ class GfmAutoComplete {
}
return null;
},
- highlighter(li, query) {
- // override default behaviour to escape dot character
- // see https://github.com/ichord/At.js/pull/576
- if (!query) {
- return li;
- }
- const escapedQuery = query.replace(/[.+]/, '\\$&');
- const regexp = new RegExp(`>\\s*([^<]*?)(${escapedQuery})([^<]*)\\s*<`, 'ig');
- return li.replace(regexp, (str, $1, $2, $3) => `> ${$1}<strong>${$2}</strong>${$3} <`);
- },
+ highlighter,
};
}
diff --git a/app/assets/javascripts/google_cloud/components/app.vue b/app/assets/javascripts/google_cloud/components/app.vue
new file mode 100644
index 00000000000..1e5be9df019
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/app.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlTab, GlTabs } from '@gitlab/ui';
+import IncubationBanner from './incubation_banner.vue';
+import ServiceAccounts from './service_accounts.vue';
+
+export default {
+ components: { GlTab, GlTabs, IncubationBanner, ServiceAccounts },
+ props: {
+ serviceAccounts: {
+ type: Array,
+ required: true,
+ },
+ createServiceAccountUrl: {
+ type: String,
+ required: true,
+ },
+ emptyIllustrationUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ feedbackUrl(template) {
+ return `https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/new?issuable_template=${template}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <incubation-banner
+ :share-feedback-url="feedbackUrl('general_feedback')"
+ :report-bug-url="feedbackUrl('report_bug')"
+ :feature-request-url="feedbackUrl('feature_request')"
+ />
+ <gl-tabs>
+ <gl-tab :title="__('Configuration')">
+ <service-accounts
+ class="gl-mx-3"
+ :list="serviceAccounts"
+ :create-url="createServiceAccountUrl"
+ :empty-illustration-url="emptyIllustrationUrl"
+ />
+ </gl-tab>
+ <gl-tab :title="__('Deployments')" disabled />
+ <gl-tab :title="__('Services')" disabled />
+ </gl-tabs>
+ </div>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/incubation_banner.vue b/app/assets/javascripts/google_cloud/components/incubation_banner.vue
new file mode 100644
index 00000000000..652b8c1aecb
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/incubation_banner.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+
+export default {
+ components: { GlAlert, GlLink, GlSprintf },
+ props: {
+ shareFeedbackUrl: {
+ required: true,
+ type: String,
+ },
+ reportBugUrl: {
+ required: true,
+ type: String,
+ },
+ featureRequestUrl: {
+ required: true,
+ type: String,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert :dismissible="false" variant="info">
+ {{ __('This is an experimental feature developed by GitLab Incubation Engineering.') }}
+ <gl-sprintf
+ :message="
+ __(
+ 'We invite you to %{featureLinkStart}request a feature%{featureLinkEnd}, %{bugLinkStart}report a bug%{bugLinkEnd} or %{feedbackLinkStart}share feedback%{feedbackLinkEnd}',
+ )
+ "
+ >
+ <template #featureLink="{ content }">
+ <gl-link :href="featureRequestUrl">{{ content }}</gl-link>
+ </template>
+ <template #bugLink="{ content }">
+ <gl-link :href="reportBugUrl">{{ content }}</gl-link>
+ </template>
+ <template #feedbackLink="{ content }">
+ <gl-link :href="shareFeedbackUrl">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/service_accounts.vue b/app/assets/javascripts/google_cloud/components/service_accounts.vue
new file mode 100644
index 00000000000..b70b25a5dc3
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/service_accounts.vue
@@ -0,0 +1,65 @@
+<script>
+import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: { GlButton, GlEmptyState, GlTable },
+ props: {
+ list: {
+ type: Array,
+ required: true,
+ },
+ createUrl: {
+ type: String,
+ required: true,
+ },
+ emptyIllustrationUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ tableFields: [
+ { key: 'environment', label: __('Environment'), sortable: true },
+ { key: 'gcp_project', label: __('Google Cloud Project'), sortable: true },
+ { key: 'service_account_exists', label: __('Service Account'), sortable: true },
+ { key: 'service_account_key_exists', label: __('Service Account Key'), sortable: true },
+ ],
+ };
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-empty-state
+ v-if="list.length === 0"
+ :title="__('No service accounts')"
+ :description="
+ __('Service Accounts keys authorize GitLab to deploy your Google Cloud project')
+ "
+ :primary-button-link="createUrl"
+ :primary-button-text="__('Create service account')"
+ :svg-path="emptyIllustrationUrl"
+ />
+
+ <div v-else>
+ <h2 class="gl-font-size-h2">{{ __('Service Accounts') }}</h2>
+ <p>{{ __('Service Accounts keys authorize GitLab to deploy your Google Cloud project') }}</p>
+
+ <gl-table :items="list" :fields="tableFields">
+ <template #cell(service_account_exists)="{ value }">
+ {{ value ? '✔' : __('Not found') }}
+ </template>
+ <template #cell(service_account_key_exists)="{ value }">
+ {{ value ? '✔' : __('Not found') }}
+ </template>
+ </gl-table>
+
+ <gl-button :href="createUrl" category="primary" variant="info">
+ {{ __('Create service account') }}
+ </gl-button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/google_cloud/index.js b/app/assets/javascripts/google_cloud/index.js
new file mode 100644
index 00000000000..a156a632e9a
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/index.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import App from './components/app.vue';
+
+const elementRenderer = (element, props = {}) => (createElement) =>
+ createElement(element, { props });
+
+export default () => {
+ const root = document.querySelector('#js-google-cloud');
+ const props = JSON.parse(root.getAttribute('data'));
+ return new Vue({ el: root, render: elementRenderer(App, props) });
+};
diff --git a/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql
new file mode 100644
index 00000000000..78a368089a8
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql
@@ -0,0 +1,4 @@
+fragment Iteration on Iteration {
+ id
+ title
+}
diff --git a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
index e345fe97281..c5f99a1657e 100644
--- a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
@@ -1,9 +1,10 @@
#import "../fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
-query usersSearch($search: String!, $fullPath: ID!) {
+query groupUsersSearch($search: String!, $fullPath: ID!) {
workspace: group(fullPath: $fullPath) {
- users: groupMembers(search: $search, relations: [DIRECT, INHERITED]) {
+ id
+ users: groupMembers(search: $search, relations: [DIRECT, DESCENDANTS, INHERITED]) {
nodes {
user {
...User
diff --git a/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql
index 1d9497d65ce..62ce27815c7 100644
--- a/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql
@@ -1,5 +1,6 @@
query searchProjectMembers($fullPath: ID!, $search: String) {
project(fullPath: $fullPath) {
+ id
projectMembers(search: $search) {
nodes {
user {
diff --git a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
index e18eea33041..d04a49f8b3a 100644
--- a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
@@ -1,7 +1,7 @@
#import "../fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
-query usersSearch($search: String!, $fullPath: ID!) {
+query projectUsersSearch($search: String!, $fullPath: ID!) {
workspace: project(fullPath: $fullPath) {
users: projectMembers(search: $search, relations: [DIRECT, INHERITED, INVITED_GROUPS]) {
nodes {
diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js
index 828ddd95ffc..8fb70eb59bd 100644
--- a/app/assets/javascripts/graphql_shared/utils.js
+++ b/app/assets/javascripts/graphql_shared/utils.js
@@ -15,6 +15,8 @@ export const isGid = (id) => {
return false;
};
+const parseGid = (gid) => parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10);
+
/**
* Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Environments/123. This method extracts Id number
@@ -23,8 +25,10 @@ export const isGid = (id) => {
* @param {String} gid GraphQL global ID
* @returns {Number}
*/
-export const getIdFromGraphQLId = (gid = '') =>
- parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10) || null;
+export const getIdFromGraphQLId = (gid = '') => {
+ const parsedGid = parseGid(gid);
+ return Number.isInteger(parsedGid) ? parsedGid : null;
+};
export const MutationOperationMode = {
Append: 'APPEND',
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index b6a1f41afb5..f255f8a084c 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,6 +1,6 @@
import createFlash from '~/flash';
import { __ } from '~/locale';
-import fetchGroupPathAvailability from '~/pages/groups/new/fetch_group_path_availability';
+import { getGroupPathAvailability } from '~/rest_api';
import { slugify } from './lib/utils/text_utility';
export default class Group {
@@ -51,7 +51,7 @@ export default class Group {
const slug = this.groupPaths[0]?.value || slugify(value);
if (!slug) return;
- fetchGroupPathAvailability(slug, this.parentId?.value)
+ getGroupPathAvailability(slug, this.parentId?.value)
.then(({ data }) => data)
.then(({ exists, suggests }) => {
if (exists && suggests.length) {
diff --git a/app/assets/javascripts/group_settings/components/shared_runners_form.vue b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
index f61d96b3dfd..dcac337c6ef 100644
--- a/app/assets/javascripts/group_settings/components/shared_runners_form.vue
+++ b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
@@ -3,13 +3,7 @@ import { GlToggle, GlLoadingIcon, GlTooltip, GlAlert } from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import {
- DEBOUNCE_TOGGLE_DELAY,
- ERROR_MESSAGE,
- ENABLED,
- DISABLED,
- ALLOW_OVERRIDE,
-} from '../constants';
+import { DEBOUNCE_TOGGLE_DELAY, ERROR_MESSAGE } from '../constants';
export default {
components: {
@@ -18,21 +12,14 @@ export default {
GlTooltip,
GlAlert,
},
- props: {
- updatePath: {
- type: String,
- required: true,
- },
- sharedRunnersAvailability: {
- type: String,
- required: true,
- },
- parentSharedRunnersAvailability: {
- type: String,
- required: false,
- default: '',
- },
- },
+ inject: [
+ 'updatePath',
+ 'sharedRunnersAvailability',
+ 'parentSharedRunnersAvailability',
+ 'runnerEnabled',
+ 'runnerDisabled',
+ 'runnerAllowOverride',
+ ],
data() {
return {
isLoading: false,
@@ -43,21 +30,21 @@ export default {
},
computed: {
toggleDisabled() {
- return this.parentSharedRunnersAvailability === DISABLED || this.isLoading;
+ return this.parentSharedRunnersAvailability === this.runnerDisabled || this.isLoading;
},
enabledOrDisabledSetting() {
- return this.enabled ? ENABLED : DISABLED;
+ return this.enabled ? this.runnerEnabled : this.runnerDisabled;
},
disabledWithOverrideSetting() {
- return this.allowOverride ? ALLOW_OVERRIDE : DISABLED;
+ return this.allowOverride ? this.runnerAllowOverride : this.runnerDisabled;
},
},
created() {
- if (this.sharedRunnersAvailability !== ENABLED) {
+ if (this.sharedRunnersAvailability !== this.runnerEnabled) {
this.enabled = false;
}
- if (this.sharedRunnersAvailability === ALLOW_OVERRIDE) {
+ if (this.sharedRunnersAvailability === this.runnerAllowOverride) {
this.allowOverride = true;
}
},
diff --git a/app/assets/javascripts/group_settings/constants.js b/app/assets/javascripts/group_settings/constants.js
index c7bb851c06b..4067b6b52a3 100644
--- a/app/assets/javascripts/group_settings/constants.js
+++ b/app/assets/javascripts/group_settings/constants.js
@@ -4,8 +4,3 @@ import { __ } from '~/locale';
export const DEBOUNCE_TOGGLE_DELAY = 1000;
export const ERROR_MESSAGE = __('Refresh the page and try again.');
-
-// runner setting options
-export const ENABLED = 'enabled';
-export const DISABLED = 'disabled_and_unoverridable';
-export const ALLOW_OVERRIDE = 'disabled_with_override';
diff --git a/app/assets/javascripts/group_settings/mount_shared_runners.js b/app/assets/javascripts/group_settings/mount_shared_runners.js
index 44284204c41..21a2373e2b1 100644
--- a/app/assets/javascripts/group_settings/mount_shared_runners.js
+++ b/app/assets/javascripts/group_settings/mount_shared_runners.js
@@ -4,12 +4,27 @@ import UpdateSharedRunnersForm from './components/shared_runners_form.vue';
export default (containerId = 'update-shared-runners-form') => {
const containerEl = document.getElementById(containerId);
+ const {
+ updatePath,
+ sharedRunnersAvailability,
+ parentSharedRunnersAvailability,
+ runnerEnabled,
+ runnerDisabled,
+ runnerAllowOverride,
+ } = containerEl.dataset;
+
return new Vue({
el: containerEl,
+ provide: {
+ updatePath,
+ sharedRunnersAvailability,
+ parentSharedRunnersAvailability,
+ runnerEnabled,
+ runnerDisabled,
+ runnerAllowOverride,
+ },
render(createElement) {
- return createElement(UpdateSharedRunnersForm, {
- props: containerEl.dataset,
- });
+ return createElement(UpdateSharedRunnersForm);
},
});
};
diff --git a/app/assets/javascripts/groups/components/item_caret.vue b/app/assets/javascripts/groups/components/item_caret.vue
index 9c379d7bf9b..a51edd385dd 100644
--- a/app/assets/javascripts/groups/components/item_caret.vue
+++ b/app/assets/javascripts/groups/components/item_caret.vue
@@ -22,6 +22,6 @@ export default {
<template>
<span class="folder-caret gl-mr-2">
- <gl-icon :size="10" :name="iconClass" use-deprecated-sizes />
+ <gl-icon :size="12" :name="iconClass" />
</span>
</template>
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index 96cb4f3d495..55ae5501cdb 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlButton, GlIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
import { throttle } from 'lodash';
import { mapActions, mapState } from 'vuex';
import { __ } from '../../../locale';
@@ -14,6 +14,7 @@ const scrollPositions = {
export default {
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
components: {
GlButton,
@@ -100,8 +101,8 @@ export default {
<pre ref="buildJobLog" class="build-log mb-0 h-100 mr-3" @scroll="scrollBuildLog">
<code
v-show="!detailJob.isLoading"
+ v-safe-html="jobOutput"
class="bash"
- v-html="jobOutput /* eslint-disable-line vue/no-v-html */"
>
</code>
<div
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index e8541d3a4c3..1c5a00568eb 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -2,7 +2,7 @@
import { GlModal, GlButton } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import createFlash from '~/flash';
-import { __, sprintf, s__ } from '~/locale';
+import { __, sprintf } from '~/locale';
import { modalTypes } from '../../constants';
import { trimPathComponents, getPathParent } from '../../utils';
@@ -58,7 +58,7 @@ export default {
if (this.modalType === modalTypes.rename) {
if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
createFlash({
- message: sprintf(s__('The name "%{name}" is already taken in this directory.'), {
+ message: sprintf(__('The name "%{name}" is already taken in this directory.'), {
name: this.entryName,
}),
fadeTransition: false,
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 907ac496982..e1caf1ba44a 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -87,7 +87,7 @@ export default {
v-if="!latestPipeline"
:empty-state-svg-path="pipelinesEmptyStateSvgPath"
:can-set-ci="true"
- class="mb-auto mt-auto"
+ class="gl-p-5"
/>
<gl-alert
v-else-if="latestPipeline.yamlError"
diff --git a/app/assets/javascripts/ide/components/shared/commit_message_field.vue b/app/assets/javascripts/ide/components/shared/commit_message_field.vue
new file mode 100644
index 00000000000..7fca7429ad7
--- /dev/null
+++ b/app/assets/javascripts/ide/components/shared/commit_message_field.vue
@@ -0,0 +1,137 @@
+<script>
+import { GlIcon, GlPopover } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
+
+export default {
+ components: {
+ GlIcon,
+ GlPopover,
+ },
+ props: {
+ text: {
+ type: String,
+ required: true,
+ },
+ placeholder: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ scrollTop: 0,
+ isFocused: false,
+ };
+ },
+ computed: {
+ allLines() {
+ return this.text.split('\n').map((line, i) => ({
+ text: line.substr(0, this.getLineLength(i)) || ' ',
+ highlightedText: line.substr(this.getLineLength(i)),
+ }));
+ },
+ },
+ methods: {
+ handleScroll() {
+ if (this.$refs.textarea) {
+ this.$nextTick(() => {
+ this.scrollTop = this.$refs.textarea.scrollTop;
+ });
+ }
+ },
+ getLineLength(i) {
+ return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
+ },
+ onInput(e) {
+ this.$emit('input', e.target.value);
+ },
+ onCtrlEnter() {
+ if (!this.isFocused) return;
+ this.$emit('submit');
+ },
+ updateIsFocused(isFocused) {
+ this.isFocused = isFocused;
+ },
+ },
+ popoverOptions: {
+ triggers: 'hover',
+ placement: 'top',
+ content: sprintf(
+ __(`
+ The character highlighter helps you keep the subject line to %{titleLength} characters
+ and wrap the body at %{bodyLength} so they are readable in git.
+ `),
+ { titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
+ ),
+ },
+};
+</script>
+
+<template>
+ <fieldset
+ class="gl-rounded-base gl-inset-border-1-gray-400 gl-py-4 gl-px-5"
+ :class="{
+ 'gl-outline-none! gl-focus-ring-border-1-gray-900!': isFocused,
+ }"
+ >
+ <div
+ v-once
+ class="gl-display-flex gl-align-items-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-pb-3 gl-mb-3"
+ >
+ <div>{{ __('Commit Message') }}</div>
+ <div id="commit-message-popover-container">
+ <span id="commit-message-question" class="gl-gray-700 gl-ml-3">
+ <gl-icon name="question" />
+ </span>
+ <gl-popover
+ target="commit-message-question"
+ container="commit-message-popover-container"
+ v-bind="$options.popoverOptions"
+ />
+ </div>
+ </div>
+ <div class="gl-relative gl-w-full gl-h-13 gl-overflow-hidden">
+ <div class="gl-absolute gl-z-index-1 gl-font-monospace gl-text-transparent">
+ <div
+ data-testid="highlights"
+ :style="{
+ transform: `translate3d(0, ${-scrollTop}px, 0)`,
+ }"
+ >
+ <div v-for="(line, index) in allLines" :key="index">
+ <span
+ data-testid="highlights-text"
+ class="gl-white-space-pre-wrap gl-word-break-word"
+ v-text="line.text"
+ >
+ </span
+ ><mark
+ v-show="line.highlightedText"
+ data-testid="highlights-mark"
+ class="gl-px-1 gl-py-0 gl-bg-orange-100 gl-text-transparent gl-white-space-pre-wrap gl-word-break-word"
+ v-text="line.highlightedText"
+ >
+ </mark>
+ </div>
+ </div>
+ </div>
+ <textarea
+ ref="textarea"
+ :placeholder="placeholder"
+ :value="text"
+ class="gl-absolute gl-w-full gl-h-full gl-z-index-2 gl-font-monospace p-0 gl-outline-0 gl-bg-transparent gl-border-0"
+ data-qa-selector="ide_commit_message_field"
+ dir="auto"
+ name="commit-message"
+ @scroll="handleScroll"
+ @input="onInput"
+ @focus="updateIsFocused(true)"
+ @blur="updateIsFocused(false)"
+ @keydown.ctrl.enter="onCtrlEnter"
+ @keydown.meta.enter="onCtrlEnter"
+ >
+ </textarea>
+ </div>
+ </fieldset>
+</template>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 4845b667b40..706d98fdb90 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -5,7 +5,7 @@ export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
export const SIDEBAR_INIT_WIDTH = 340;
-export const SIDEBAR_MIN_WIDTH = 340;
+export const SIDEBAR_MIN_WIDTH = 260;
export const SIDEBAR_NAV_WIDTH = 60;
// File view modes
diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue
index 5ba910746ca..25d4037bbe5 100644
--- a/app/assets/javascripts/import_entities/components/group_dropdown.vue
+++ b/app/assets/javascripts/import_entities/components/group_dropdown.vue
@@ -19,7 +19,7 @@ export default {
computed: {
filteredNamespaces() {
return this.namespaces.filter((ns) =>
- ns.toLowerCase().includes(this.searchTerm.toLowerCase()),
+ ns.fullPath.toLowerCase().includes(this.searchTerm.toLowerCase()),
);
},
},
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
index 104c84173fc..e004bc35087 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
@@ -1,7 +1,5 @@
<script>
import { GlButton, GlIcon, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { isFinished, isInvalid, isAvailableForImport } from '../utils';
export default {
components: {
@@ -12,32 +10,17 @@ export default {
GlTooltip,
},
props: {
- group: {
- type: Object,
+ isFinished: {
+ type: Boolean,
required: true,
},
- groupPathRegex: {
- type: RegExp,
+ isAvailableForImport: {
+ type: Boolean,
required: true,
},
- },
- computed: {
- fullLastImportPath() {
- return this.group.last_import_target
- ? `${this.group.last_import_target.target_namespace}/${this.group.last_import_target.new_name}`
- : null;
- },
- absoluteLastImportPath() {
- return joinPaths(gon.relative_url_root || '/', this.fullLastImportPath);
- },
- isAvailableForImport() {
- return isAvailableForImport(this.group);
- },
- isFinished() {
- return isFinished(this.group);
- },
- isInvalid() {
- return isInvalid(this.group, this.groupPathRegex);
+ isInvalid: {
+ type: Boolean,
+ required: true,
},
},
};
@@ -56,7 +39,7 @@ export default {
{{ isFinished ? __('Re-import') : __('Import') }}
</gl-button>
<gl-icon
- v-if="isFinished"
+ v-if="isAvailableForImport && isFinished"
v-gl-tooltip
:size="16"
name="information-o"
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue
index 2de9bd4f868..cad1b983d61 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue
@@ -1,7 +1,6 @@
<script>
import { GlLink, GlSprintf, GlIcon } from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
-import { isFinished } from '../utils';
export default {
components: {
@@ -17,16 +16,13 @@ export default {
},
computed: {
fullLastImportPath() {
- return this.group.last_import_target
- ? `${this.group.last_import_target.target_namespace}/${this.group.last_import_target.new_name}`
+ return this.group.lastImportTarget
+ ? `${this.group.lastImportTarget.targetNamespace}/${this.group.lastImportTarget.newName}`
: null;
},
absoluteLastImportPath() {
return joinPaths(gon.relative_url_root || '/', this.fullLastImportPath);
},
- isFinished() {
- return isFinished(this.group);
- },
},
};
</script>
@@ -34,13 +30,13 @@ export default {
<template>
<div>
<gl-link
- :href="group.web_url"
+ :href="group.webUrl"
target="_blank"
class="gl-display-inline-flex gl-align-items-center gl-h-7"
>
- {{ group.full_path }} <gl-icon name="external-link" />
+ {{ group.fullPath }} <gl-icon name="external-link" />
</gl-link>
- <div v-if="isFinished && fullLastImportPath" class="gl-font-sm">
+ <div v-if="group.flags.isFinished && fullLastImportPath" class="gl-font-sm">
<gl-sprintf :message="s__('BulkImport|Last imported to %{link}')">
<template #link>
<gl-link :href="absoluteLastImportPath" class="gl-font-sm" target="_blank">{{
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index 04b037ecc2b..ec6025c84bb 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -12,18 +12,28 @@ import {
GlTable,
GlFormCheckbox,
} from '@gitlab/ui';
+import { debounce } from 'lodash';
+import createFlash from '~/flash';
import { s__, __, n__ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import { getGroupPathAvailability } from '~/rest_api';
+import axios from '~/lib/utils/axios_utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+import { STATUSES } from '../../constants';
import ImportStatusCell from '../../components/import_status.vue';
import importGroupsMutation from '../graphql/mutations/import_groups.mutation.graphql';
-import setImportTargetMutation from '../graphql/mutations/set_import_target.mutation.graphql';
+import updateImportStatusMutation from '../graphql/mutations/update_import_status.mutation.graphql';
import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
-import { isInvalid, isFinished, isAvailableForImport } from '../utils';
+import { NEW_NAME_FIELD, i18n } from '../constants';
+import { StatusPoller } from '../services/status_poller';
+import { isFinished, isAvailableForImport, isNameValid, isSameTarget } from '../utils';
import ImportActionsCell from './import_actions_cell.vue';
import ImportSourceCell from './import_source_cell.vue';
import ImportTargetCell from './import_target_cell.vue';
+const VALIDATION_DEBOUNCE_TIME = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
const PAGE_SIZES = [20, 50, 100];
const DEFAULT_PAGE_SIZE = PAGE_SIZES[0];
const DEFAULT_TH_CLASSES =
@@ -59,7 +69,7 @@ export default {
type: RegExp,
required: true,
},
- groupUrlErrorMessage: {
+ jobsPath: {
type: String,
required: true,
},
@@ -70,7 +80,9 @@ export default {
filter: '',
page: 1,
perPage: DEFAULT_PAGE_SIZE,
- selectedGroups: [],
+ selectedGroupsIds: [],
+ pendingGroupsIds: [],
+ importTargets: {},
};
},
@@ -94,14 +106,14 @@ export default {
tdClass: `${DEFAULT_TD_CLASSES} gl-pr-3!`,
},
{
- key: 'web_url',
+ key: 'webUrl',
label: s__('BulkImport|From source group'),
thClass: `${DEFAULT_TH_CLASSES} gl-pl-0! import-jobs-from-col`,
// eslint-disable-next-line @gitlab/require-i18n-strings
tdClass: `${DEFAULT_TD_CLASSES} gl-pl-0!`,
},
{
- key: 'import_target',
+ key: 'importTarget',
label: s__('BulkImport|To new group'),
thClass: `${DEFAULT_TH_CLASSES} import-jobs-to-col`,
tdClass: DEFAULT_TD_CLASSES,
@@ -126,16 +138,39 @@ export default {
return this.bulkImportSourceGroups?.nodes ?? [];
},
+ groupsTableData() {
+ return this.groups.map((group) => {
+ const importTarget = this.getImportTarget(group);
+ const status = this.getStatus(group);
+
+ const flags = {
+ isInvalid: importTarget.validationErrors?.length > 0,
+ isAvailableForImport: isAvailableForImport(group) && status !== STATUSES.SCHEDULING,
+ isFinished: isFinished(group),
+ };
+
+ return {
+ ...group,
+ visibleStatus: status,
+ importTarget,
+ flags: {
+ ...flags,
+ isUnselectable: !flags.isAvailableForImport || flags.isInvalid,
+ },
+ };
+ });
+ },
+
hasSelectedGroups() {
- return this.selectedGroups.length > 0;
+ return this.selectedGroupsIds.length > 0;
},
hasAllAvailableGroupsSelected() {
- return this.selectedGroups.length === this.availableGroupsForImport.length;
+ return this.selectedGroupsIds.length === this.availableGroupsForImport.length;
},
availableGroupsForImport() {
- return this.groups.filter((g) => isAvailableForImport(g) && !this.isInvalid(g));
+ return this.groupsTableData.filter((g) => g.flags.isAvailableForImport && g.flags.isInvalid);
},
humanizedTotal() {
@@ -175,25 +210,43 @@ export default {
filter() {
this.page = 1;
},
- groups() {
+
+ groupsTableData() {
const table = this.getTableRef();
- this.groups.forEach((g, idx) => {
- if (this.selectedGroups.includes(g)) {
+ const matches = new Set();
+ this.groupsTableData.forEach((g, idx) => {
+ if (this.selectedGroupsIds.includes(g.id)) {
+ matches.add(g.id);
this.$nextTick(() => {
table.selectRow(idx);
});
}
});
- this.selectedGroups = [];
+
+ this.selectedGroupsIds = this.selectedGroupsIds.filter((id) => matches.has(id));
},
},
- methods: {
- isUnselectable(group) {
- return !this.isAvailableForImport(group) || this.isInvalid(group);
- },
+ mounted() {
+ this.statusPoller = new StatusPoller({
+ pollPath: this.jobsPath,
+ updateImportStatus: (update) => {
+ this.$apollo.mutate({
+ mutation: updateImportStatusMutation,
+ variables: { id: update.id, status: update.status_name },
+ });
+ },
+ });
- rowClasses(group) {
+ this.statusPoller.startPolling();
+ },
+
+ beforeDestroy() {
+ this.statusPoller.stopPolling();
+ },
+
+ methods: {
+ rowClasses(groupTableItem) {
const DEFAULT_CLASSES = [
'gl-border-gray-200',
'gl-border-0',
@@ -201,7 +254,7 @@ export default {
'gl-border-solid',
];
const result = [...DEFAULT_CLASSES];
- if (this.isUnselectable(group)) {
+ if (groupTableItem.flags.isUnselectable) {
result.push('gl-cursor-default!');
}
return result;
@@ -211,19 +264,13 @@ export default {
if (type === 'row') {
return {
'data-qa-selector': 'import_item',
- 'data-qa-source-group': group.full_path,
+ 'data-qa-source-group': group.fullPath,
};
}
return {};
},
- isAvailableForImport,
- isFinished,
- isInvalid(group) {
- return isInvalid(group, this.groupPathRegex);
- },
-
groupsCount(count) {
return n__('%d group', '%d groups', count);
},
@@ -232,22 +279,64 @@ export default {
this.page = page;
},
- updateImportTarget(sourceGroupId, targetNamespace, newName) {
- this.$apollo.mutate({
- mutation: setImportTargetMutation,
- variables: { sourceGroupId, targetNamespace, newName },
- });
+ getStatus(group) {
+ if (this.pendingGroupsIds.includes(group.id)) {
+ return STATUSES.SCHEDULING;
+ }
+
+ return group.progress?.status || STATUSES.NONE;
},
- importGroups(sourceGroupIds) {
- this.$apollo.mutate({
- mutation: importGroupsMutation,
- variables: { sourceGroupIds },
+ updateImportTarget(group, changes) {
+ const newImportTarget = {
+ ...group.importTarget,
+ ...changes,
+ };
+ this.$set(this.importTargets, group.id, newImportTarget);
+ this.validateImportTarget(newImportTarget);
+ },
+
+ async importGroups(importRequests) {
+ const newPendingGroupsIds = importRequests.map((request) => request.sourceGroupId);
+ newPendingGroupsIds.forEach((id) => {
+ this.importTargets[id].validationErrors = [
+ { field: NEW_NAME_FIELD, message: i18n.ERROR_IMPORT_COMPLETED },
+ ];
+
+ if (!this.pendingGroupsIds.includes(id)) {
+ this.pendingGroupsIds.push(id);
+ }
});
+
+ try {
+ await this.$apollo.mutate({
+ mutation: importGroupsMutation,
+ variables: { importRequests },
+ });
+ } catch (error) {
+ const message = error?.networkError?.response?.data?.error ?? i18n.ERROR_IMPORT;
+ createFlash({
+ message,
+ captureError: true,
+ error,
+ });
+ } finally {
+ this.pendingGroupsIds = this.pendingGroupsIds.filter(
+ (id) => !newPendingGroupsIds.includes(id),
+ );
+ }
},
importSelectedGroups() {
- this.importGroups(this.selectedGroups.map((g) => g.id));
+ const importRequests = this.groupsTableData
+ .filter((group) => this.selectedGroupsIds.includes(group.id))
+ .map((group) => ({
+ sourceGroupId: group.id,
+ targetNamespace: group.importTarget.targetNamespace.fullPath,
+ newName: group.importTarget.newName,
+ }));
+
+ this.importGroups(importRequests);
},
setPageSize(size) {
@@ -263,16 +352,115 @@ export default {
preventSelectingAlreadyImportedGroups(updatedSelection) {
if (updatedSelection) {
- this.selectedGroups = updatedSelection;
+ this.selectedGroupsIds = updatedSelection.map((g) => g.id);
}
const table = this.getTableRef();
- this.groups.forEach((group, idx) => {
- if (table.isRowSelected(idx) && this.isUnselectable(group)) {
+ this.groupsTableData.forEach((group, idx) => {
+ if (table.isRowSelected(idx) && group.flags.isUnselectable) {
table.unselectRow(idx);
}
});
},
+
+ validateImportTarget: debounce(async function validate(importTarget) {
+ const newValidationErrors = [];
+ importTarget.cancellationToken?.cancel();
+ if (importTarget.newName === '') {
+ newValidationErrors.push({ field: NEW_NAME_FIELD, message: i18n.ERROR_REQUIRED });
+ } else if (!isNameValid(importTarget, this.groupPathRegex)) {
+ newValidationErrors.push({ field: NEW_NAME_FIELD, message: i18n.ERROR_INVALID_FORMAT });
+ } else if (Object.values(this.importTargets).find(isSameTarget(importTarget))) {
+ newValidationErrors.push({
+ field: NEW_NAME_FIELD,
+ message: i18n.ERROR_NAME_ALREADY_USED_IN_SUGGESTION,
+ });
+ } else {
+ try {
+ // eslint-disable-next-line no-param-reassign
+ importTarget.cancellationToken = axios.CancelToken.source();
+ const {
+ data: { exists },
+ } = await getGroupPathAvailability(
+ importTarget.newName,
+ importTarget.targetNamespace.id,
+ {
+ cancelToken: importTarget.cancellationToken?.token,
+ },
+ );
+
+ if (exists) {
+ newValidationErrors.push({
+ field: NEW_NAME_FIELD,
+ message: i18n.ERROR_NAME_ALREADY_EXISTS,
+ });
+ }
+ } catch (e) {
+ if (!axios.isCancel(e)) {
+ throw e;
+ }
+ }
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ importTarget.validationErrors = newValidationErrors;
+ }, VALIDATION_DEBOUNCE_TIME),
+
+ getImportTarget(group) {
+ if (this.importTargets[group.id]) {
+ return this.importTargets[group.id];
+ }
+
+ const defaultTargetNamespace = this.availableNamespaces[0] ?? { fullPath: '', id: null };
+ let importTarget;
+ if (group.lastImportTarget) {
+ const targetNamespace = this.availableNamespaces.find(
+ (ns) => ns.fullPath === group.lastImportTarget.targetNamespace,
+ );
+
+ importTarget = {
+ targetNamespace: targetNamespace ?? defaultTargetNamespace,
+ newName: group.lastImportTarget.newName,
+ };
+ } else {
+ importTarget = {
+ targetNamespace: defaultTargetNamespace,
+ newName: group.fullPath,
+ };
+ }
+
+ const cancellationToken = axios.CancelToken.source();
+ this.$set(this.importTargets, group.id, {
+ ...importTarget,
+ cancellationToken,
+ validationErrors: [],
+ });
+
+ getGroupPathAvailability(importTarget.newName, importTarget.targetNamespace.id, {
+ cancelToken: cancellationToken.token,
+ })
+ .then(({ data: { exists, suggests: suggestions } }) => {
+ if (!exists) return;
+
+ let currentSuggestion = suggestions[0] ?? importTarget.newName;
+ const existingTargets = Object.values(this.importTargets)
+ .filter((t) => t.targetNamespace.id === importTarget.targetNamespace.id)
+ .map((t) => t.newName.toLowerCase());
+
+ while (existingTargets.includes(currentSuggestion.toLowerCase())) {
+ currentSuggestion = `${currentSuggestion}-1`;
+ }
+
+ Object.assign(this.importTargets[group.id], {
+ targetNamespace: importTarget.targetNamespace,
+ newName: currentSuggestion,
+ });
+ })
+ .catch(() => {
+ // empty catch intended
+ });
+ return this.importTargets[group.id];
+ },
},
gitlabLogo: window.gon.gitlab_logo,
@@ -329,7 +517,7 @@ export default {
<gl-empty-state
v-else-if="!hasGroups"
:title="s__('BulkImport|You have no groups to import')"
- :description="s__('Check your source instance permissions.')"
+ :description="__('Check your source instance permissions.')"
/>
<template v-else>
<div
@@ -337,7 +525,7 @@ export default {
>
<gl-sprintf :message="__('%{count} selected')">
<template #count>
- {{ selectedGroups.length }}
+ {{ selectedGroupsIds.length }}
</template>
</gl-sprintf>
<gl-button
@@ -355,7 +543,7 @@ export default {
data-qa-selector="import_table"
:tbody-tr-class="rowClasses"
:tbody-tr-attr="qaRowAttributes"
- :items="groups"
+ :items="groupsTableData"
:fields="$options.fields"
selectable
select-mode="multi"
@@ -364,7 +552,7 @@ export default {
>
<template #head(selected)="{ selectAllRows, clearSelected }">
<gl-form-checkbox
- :key="`checkbox-${selectedGroups.length}`"
+ :key="`checkbox-${selectedGroupsIds.length}`"
class="gl-h-7 gl-pt-3"
:checked="hasSelectedGroups"
:indeterminate="hasSelectedGroups && !hasAllAvailableGroupsSelected"
@@ -375,35 +563,39 @@ export default {
<gl-form-checkbox
class="gl-h-7 gl-pt-3"
:checked="rowSelected"
- :disabled="!isAvailableForImport(group) || isInvalid(group)"
+ :disabled="group.flags.isUnselectable"
@change="rowSelected ? unselectRow() : selectRow()"
/>
</template>
- <template #cell(web_url)="{ item: group }">
+ <template #cell(webUrl)="{ item: group }">
<import-source-cell :group="group" />
</template>
- <template #cell(import_target)="{ item: group }">
+ <template #cell(importTarget)="{ item: group }">
<import-target-cell
:group="group"
:available-namespaces="availableNamespaces"
:group-path-regex="groupPathRegex"
- :group-url-error-message="groupUrlErrorMessage"
- @update-target-namespace="
- updateImportTarget(group.id, $event, group.import_target.new_name)
- "
- @update-new-name="
- updateImportTarget(group.id, group.import_target.target_namespace, $event)
- "
+ @update-target-namespace="updateImportTarget(group, { targetNamespace: $event })"
+ @update-new-name="updateImportTarget(group, { newName: $event })"
/>
</template>
- <template #cell(progress)="{ value: { status } }">
- <import-status-cell :status="status" class="gl-line-height-32" />
+ <template #cell(progress)="{ item: group }">
+ <import-status-cell :status="group.visibleStatus" class="gl-line-height-32" />
</template>
<template #cell(actions)="{ item: group }">
<import-actions-cell
- :group="group"
- :group-path-regex="groupPathRegex"
- @import-group="importGroups([group.id])"
+ :is-finished="group.flags.isFinished"
+ :is-available-for-import="group.flags.isAvailableForImport"
+ :is-invalid="group.flags.isInvalid"
+ @import-group="
+ importGroups([
+ {
+ sourceGroupId: group.id,
+ targetNamespace: group.importTarget.targetNamespace.fullPath,
+ newName: group.importTarget.newName,
+ },
+ ])
+ "
/>
</template>
</gl-table>
@@ -413,7 +605,7 @@ export default {
:page-info="bulkImportSourceGroups.pageInfo"
class="gl-m-0"
/>
- <gl-dropdown category="tertiary" class="gl-ml-auto">
+ <gl-dropdown category="tertiary" :aria-label="__('Page size')" class="gl-ml-auto">
<template #button-content>
<span class="font-weight-bold">
<gl-sprintf :message="__('%{count} items per page')">
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
index daced740c94..ca9ae9447d0 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
@@ -7,12 +7,7 @@ import {
} from '@gitlab/ui';
import { s__ } from '~/locale';
import ImportGroupDropdown from '../../components/group_dropdown.vue';
-import {
- isInvalid,
- getInvalidNameValidationMessage,
- isNameValid,
- isAvailableForImport,
-} from '../utils';
+import { getInvalidNameValidationMessage } from '../utils';
export default {
components: {
@@ -31,44 +26,15 @@ export default {
type: Array,
required: true,
},
- groupPathRegex: {
- type: RegExp,
- required: true,
- },
- groupUrlErrorMessage: {
- type: String,
- required: true,
- },
},
computed: {
- availableNamespaceNames() {
- return this.availableNamespaces.map((ns) => ns.full_path);
- },
-
- importTarget() {
- return this.group.import_target;
+ fullPath() {
+ return this.group.importTarget.targetNamespace.fullPath || s__('BulkImport|No parent');
},
-
invalidNameValidationMessage() {
- return getInvalidNameValidationMessage(this.group);
+ return getInvalidNameValidationMessage(this.group.importTarget);
},
-
- isInvalid() {
- return isInvalid(this.group, this.groupPathRegex);
- },
-
- isNameValid() {
- return isNameValid(this.group, this.groupPathRegex);
- },
-
- isAvailableForImport() {
- return isAvailableForImport(this.group);
- },
- },
-
- i18n: {
- NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'),
},
};
</script>
@@ -77,14 +43,14 @@ export default {
<div class="gl-display-flex gl-align-items-stretch">
<import-group-dropdown
#default="{ namespaces }"
- :text="importTarget.target_namespace"
- :disabled="!isAvailableForImport"
- :namespaces="availableNamespaceNames"
+ :text="fullPath"
+ :disabled="!group.flags.isAvailableForImport"
+ :namespaces="availableNamespaces"
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
class="gl-h-7 gl-flex-grow-1"
data-qa-selector="target_namespace_selector_dropdown"
>
- <gl-dropdown-item @click="$emit('update-target-namespace', '')">{{
+ <gl-dropdown-item @click="$emit('update-target-namespace', { fullPath: '', id: null })">{{
s__('BulkImport|No parent')
}}</gl-dropdown-item>
<template v-if="namespaces.length">
@@ -94,20 +60,20 @@ export default {
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="ns in namespaces"
- :key="ns"
+ :key="ns.fullPath"
data-qa-selector="target_group_dropdown_item"
- :data-qa-group-name="ns"
+ :data-qa-group-name="ns.fullPath"
@click="$emit('update-target-namespace', ns)"
>
- {{ ns }}
+ {{ ns.fullPath }}
</gl-dropdown-item>
</template>
</import-group-dropdown>
<div
class="gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1 gl-bg-gray-10"
:class="{
- 'gl-text-gray-400 gl-border-gray-100': !isAvailableForImport,
- 'gl-border-gray-200': isAvailableForImport,
+ 'gl-text-gray-400 gl-border-gray-100': !group.flags.isAvailableForImport,
+ 'gl-border-gray-200': group.flags.isAvailableForImport,
}"
>
/
@@ -116,21 +82,21 @@ export default {
<gl-form-input
class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
:class="{
- 'gl-inset-border-1-gray-200!': isAvailableForImport,
- 'gl-inset-border-1-gray-100!': !isAvailableForImport,
- 'is-invalid': isInvalid && isAvailableForImport,
+ 'gl-inset-border-1-gray-200!': group.flags.isAvailableForImport,
+ 'gl-inset-border-1-gray-100!': !group.flags.isAvailableForImport,
+ 'is-invalid': group.flags.isInvalid && group.flags.isAvailableForImport,
}"
- :disabled="!isAvailableForImport"
- :value="importTarget.new_name"
+ debounce="500"
+ :disabled="!group.flags.isAvailableForImport"
+ :value="group.importTarget.newName"
+ :aria-label="__('New name')"
@input="$emit('update-new-name', $event)"
/>
- <p v-if="isInvalid" class="gl-text-red-500 gl-m-0 gl-mt-2">
- <template v-if="!isNameValid">
- {{ groupUrlErrorMessage }}
- </template>
- <template v-else-if="invalidNameValidationMessage">
- {{ invalidNameValidationMessage }}
- </template>
+ <p
+ v-if="group.flags.isAvailableForImport && group.flags.isInvalid"
+ class="gl-text-red-500 gl-m-0 gl-mt-2"
+ >
+ {{ invalidNameValidationMessage }}
</p>
</div>
</div>
diff --git a/app/assets/javascripts/import_entities/import_groups/constants.js b/app/assets/javascripts/import_entities/import_groups/constants.js
index b2c3d85e280..aa9cf3897e6 100644
--- a/app/assets/javascripts/import_entities/import_groups/constants.js
+++ b/app/assets/javascripts/import_entities/import_groups/constants.js
@@ -1,7 +1,16 @@
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
export const i18n = {
- NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'),
+ ERROR_INVALID_FORMAT: s__(
+ 'GroupSettings|Please choose a group URL with no special characters or spaces.',
+ ),
+ ERROR_NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'),
+ ERROR_REQUIRED: __('This field is required.'),
+ ERROR_NAME_ALREADY_USED_IN_SUGGESTION: s__(
+ 'BulkImport|Name already used as a target for another group.',
+ ),
+ ERROR_IMPORT: s__('BulkImport|Importing the group failed.'),
+ ERROR_IMPORT_COMPLETED: s__('BulkImport|Import is finished. Pick another name for re-import'),
};
-export const NEW_NAME_FIELD = 'new_name';
+export const NEW_NAME_FIELD = 'newName';
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
index c08cf909a00..bce6e7bcb1f 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
@@ -1,23 +1,10 @@
-import createFlash from '~/flash';
import createDefaultClient from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
-import { s__ } from '~/locale';
import { STATUSES } from '../../constants';
-import { i18n, NEW_NAME_FIELD } from '../constants';
-import { isAvailableForImport } from '../utils';
import bulkImportSourceGroupItemFragment from './fragments/bulk_import_source_group_item.fragment.graphql';
import bulkImportSourceGroupProgressFragment from './fragments/bulk_import_source_group_progress.fragment.graphql';
-import addValidationErrorMutation from './mutations/add_validation_error.mutation.graphql';
-import removeValidationErrorMutation from './mutations/remove_validation_error.mutation.graphql';
-import setImportProgressMutation from './mutations/set_import_progress.mutation.graphql';
-import setImportTargetMutation from './mutations/set_import_target.mutation.graphql';
-import updateImportStatusMutation from './mutations/update_import_status.mutation.graphql';
-import availableNamespacesQuery from './queries/available_namespaces.query.graphql';
-import bulkImportSourceGroupQuery from './queries/bulk_import_source_group.query.graphql';
-import groupAndProjectQuery from './queries/group_and_project.query.graphql';
-import { SourceGroupsManager } from './services/source_groups_manager';
-import { StatusPoller } from './services/status_poller';
+import { LocalStorageCache } from './services/local_storage_cache';
import typeDefs from './typedefs.graphql';
export const clientTypenames = {
@@ -27,221 +14,99 @@ export const clientTypenames = {
BulkImportPageInfo: 'ClientBulkImportPageInfo',
BulkImportTarget: 'ClientBulkImportTarget',
BulkImportProgress: 'ClientBulkImportProgress',
- BulkImportValidationError: 'ClientBulkImportValidationError',
};
-function makeGroup(data) {
- const result = {
- __typename: clientTypenames.BulkImportSourceGroup,
+function makeLastImportTarget(data) {
+ return {
+ __typename: clientTypenames.BulkImportTarget,
...data,
};
- const NESTED_OBJECT_FIELDS = {
- import_target: clientTypenames.BulkImportTarget,
- last_import_target: clientTypenames.BulkImportTarget,
- progress: clientTypenames.BulkImportProgress,
- };
-
- Object.entries(NESTED_OBJECT_FIELDS).forEach(([field, type]) => {
- if (!data[field]) {
- return;
- }
- result[field] = {
- __typename: type,
- ...data[field],
- };
- });
-
- return result;
}
-async function checkImportTargetIsValid({ client, newName, targetNamespace, sourceGroupId }) {
- const {
- data: { existingGroup, existingProject },
- } = await client.query({
- query: groupAndProjectQuery,
- fetchPolicy: 'no-cache',
- variables: {
- fullPath: `${targetNamespace}/${newName}`,
- },
- });
-
- const variables = {
- field: NEW_NAME_FIELD,
- sourceGroupId,
+function makeProgress(data) {
+ return {
+ __typename: clientTypenames.BulkImportProgress,
+ ...data,
};
-
- if (!existingGroup && !existingProject) {
- client.mutate({
- mutation: removeValidationErrorMutation,
- variables,
- });
- } else {
- client.mutate({
- mutation: addValidationErrorMutation,
- variables: {
- ...variables,
- message: i18n.NAME_ALREADY_EXISTS,
- },
- });
- }
}
-const localProgressId = (id) => `not-started-${id}`;
-const nextName = (name) => `${name}-1`;
+function makeGroup(data) {
+ return {
+ __typename: clientTypenames.BulkImportSourceGroup,
+ ...data,
+ progress: data.progress
+ ? makeProgress({
+ id: `LOCAL-PROGRESS-${data.id}`,
+ ...data.progress,
+ })
+ : null,
+ lastImportTarget: data.lastImportTarget
+ ? makeLastImportTarget({
+ id: data.id,
+ ...data.lastImportTarget,
+ })
+ : null,
+ };
+}
-export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) {
- const groupsManager = new GroupsManager({
- sourceUrl,
+function getGroupFromCache({ client, id, getCacheKey }) {
+ return client.readFragment({
+ fragment: bulkImportSourceGroupItemFragment,
+ fragmentName: 'BulkImportSourceGroupItem',
+ id: getCacheKey({
+ __typename: clientTypenames.BulkImportSourceGroup,
+ id,
+ }),
});
+}
- let statusPoller;
+export function createResolvers({ endpoints }) {
+ const localStorageCache = new LocalStorageCache();
return {
Query: {
- async bulkImportSourceGroup(_, { id }, { client, getCacheKey }) {
- return client.readFragment({
- fragment: bulkImportSourceGroupItemFragment,
- fragmentName: 'BulkImportSourceGroupItem',
- id: getCacheKey({
- __typename: clientTypenames.BulkImportSourceGroup,
- id,
- }),
+ async bulkImportSourceGroups(_, vars) {
+ const { headers, data } = await axios.get(endpoints.status, {
+ params: {
+ page: vars.page,
+ per_page: vars.perPage,
+ filter: vars.filter,
+ },
});
- },
- async bulkImportSourceGroups(_, vars, { client }) {
- if (!statusPoller) {
- statusPoller = new StatusPoller({
- updateImportStatus: ({ id, status_name: status }) =>
- client.mutate({
- mutation: updateImportStatusMutation,
- variables: { id, status },
- }),
- pollPath: endpoints.jobs,
- });
- statusPoller.startPolling();
- }
-
- return Promise.all([
- axios.get(endpoints.status, {
- params: {
- page: vars.page,
- per_page: vars.perPage,
- filter: vars.filter,
- },
- }),
- client.query({ query: availableNamespacesQuery }),
- ]).then(
- ([
- { headers, data },
- {
- data: { availableNamespaces },
- },
- ]) => {
- const pagination = parseIntPagination(normalizeHeaders(headers));
-
- const response = {
- __typename: clientTypenames.BulkImportSourceGroupConnection,
- nodes: data.importable_data.map((group) => {
- const { jobId, importState: cachedImportState } =
- groupsManager.getImportStateFromStorageByGroupId(group.id) ?? {};
-
- const status = cachedImportState?.status ?? STATUSES.NONE;
-
- const importTarget =
- status === STATUSES.FINISHED && cachedImportState.importTarget
- ? {
- target_namespace: cachedImportState.importTarget.target_namespace,
- new_name: nextName(cachedImportState.importTarget.new_name),
- }
- : cachedImportState?.importTarget ?? {
- new_name: group.full_path,
- target_namespace: availableNamespaces[0]?.full_path ?? '',
- };
-
- return makeGroup({
- ...group,
- validation_errors: [],
- progress: {
- id: jobId ?? localProgressId(group.id),
- status,
- },
- import_target: importTarget,
- last_import_target: cachedImportState?.importTarget ?? null,
- });
- }),
- pageInfo: {
- __typename: clientTypenames.BulkImportPageInfo,
- ...pagination,
- },
- };
-
- setTimeout(() => {
- response.nodes.forEach((group) => {
- if (isAvailableForImport(group)) {
- checkImportTargetIsValid({
- client,
- newName: group.import_target.new_name,
- targetNamespace: group.import_target.target_namespace,
- sourceGroupId: group.id,
- });
- }
- });
+ const pagination = parseIntPagination(normalizeHeaders(headers));
+
+ const response = {
+ __typename: clientTypenames.BulkImportSourceGroupConnection,
+ nodes: data.importable_data.map((group) => {
+ return makeGroup({
+ id: group.id,
+ webUrl: group.web_url,
+ fullPath: group.full_path,
+ fullName: group.full_name,
+ ...group,
+ ...localStorageCache.get(group.web_url),
});
-
- return response;
+ }),
+ pageInfo: {
+ __typename: clientTypenames.BulkImportPageInfo,
+ ...pagination,
},
- );
+ };
+ return response;
},
availableNamespaces: () =>
axios.get(endpoints.availableNamespaces).then(({ data }) =>
data.map((namespace) => ({
__typename: clientTypenames.AvailableNamespace,
- ...namespace,
+ id: namespace.id,
+ fullPath: namespace.full_path,
})),
),
},
Mutation: {
- setImportTarget(_, { targetNamespace, newName, sourceGroupId }, { client }) {
- checkImportTargetIsValid({
- client,
- sourceGroupId,
- targetNamespace,
- newName,
- });
-
- return makeGroup({
- id: sourceGroupId,
- import_target: {
- target_namespace: targetNamespace,
- new_name: newName,
- id: sourceGroupId,
- },
- });
- },
-
- async setImportProgress(_, { sourceGroupId, status, jobId, importTarget }) {
- if (jobId) {
- groupsManager.updateImportProgress(jobId, status);
- }
-
- return makeGroup({
- id: sourceGroupId,
- progress: {
- id: jobId ?? localProgressId(sourceGroupId),
- status,
- },
- last_import_target: {
- __typename: clientTypenames.BulkImportTarget,
- ...importTarget,
- },
- });
- },
-
async updateImportStatus(_, { id, status: newStatus }, { client, getCacheKey }) {
- groupsManager.updateImportProgress(id, newStatus);
-
const progressItem = client.readFragment({
fragment: bulkImportSourceGroupProgressFragment,
fragmentName: 'BulkImportSourceGroupProgress',
@@ -251,133 +116,62 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
}),
});
- const isInProgress = Boolean(progressItem);
- const { status: currentStatus } = progressItem ?? {};
- if (newStatus === STATUSES.FINISHED && isInProgress && currentStatus !== newStatus) {
- const groups = groupsManager.getImportedGroupsByJobId(id);
+ if (!progressItem) return null;
- groups.forEach(async ({ id: groupId, importTarget }) => {
- client.mutate({
- mutation: setImportTargetMutation,
- variables: {
- sourceGroupId: groupId,
- targetNamespace: importTarget.target_namespace,
- newName: nextName(importTarget.new_name),
- },
- });
- });
- }
+ localStorageCache.updateStatusByJobId(id, newStatus);
return {
__typename: clientTypenames.BulkImportProgress,
+ ...progressItem,
id,
status: newStatus,
};
},
- async addValidationError(_, { sourceGroupId, field, message }, { client }) {
- const {
- data: {
- bulkImportSourceGroup: { validation_errors: validationErrors, ...group },
- },
- } = await client.query({
- query: bulkImportSourceGroupQuery,
- variables: { id: sourceGroupId },
- });
+ async importGroups(_, { importRequests }, { client, getCacheKey }) {
+ const importOperations = importRequests.map((importRequest) => {
+ const group = getGroupFromCache({
+ client,
+ getCacheKey,
+ id: importRequest.sourceGroupId,
+ });
- return {
- ...group,
- validation_errors: [
- ...validationErrors.filter(({ field: f }) => f !== field),
- {
- __typename: clientTypenames.BulkImportValidationError,
- field,
- message,
- },
- ],
- };
- },
+ return {
+ group,
+ ...importRequest,
+ };
+ });
- async removeValidationError(_, { sourceGroupId, field }, { client }) {
const {
- data: {
- bulkImportSourceGroup: { validation_errors: validationErrors, ...group },
- },
- } = await client.query({
- query: bulkImportSourceGroupQuery,
- variables: { id: sourceGroupId },
+ data: { id: jobId },
+ } = await axios.post(endpoints.createBulkImport, {
+ bulk_import: importOperations.map((op) => ({
+ source_type: 'group_entity',
+ source_full_path: op.group.fullPath,
+ destination_namespace: op.targetNamespace,
+ destination_name: op.newName,
+ })),
});
- return {
- ...group,
- validation_errors: validationErrors.filter(({ field: f }) => f !== field),
- };
- },
-
- async importGroups(_, { sourceGroupIds }, { client }) {
- const groups = await Promise.all(
- sourceGroupIds.map((id) =>
- client
- .query({
- query: bulkImportSourceGroupQuery,
- variables: { id },
- })
- .then(({ data }) => data.bulkImportSourceGroup),
- ),
- );
+ return importOperations.map((op) => {
+ const lastImportTarget = {
+ targetNamespace: op.targetNamespace,
+ newName: op.newName,
+ };
- const GROUPS_BEING_SCHEDULED = sourceGroupIds.map((sourceGroupId) =>
- makeGroup({
- id: sourceGroupId,
- progress: {
- id: localProgressId(sourceGroupId),
- status: STATUSES.SCHEDULING,
- },
- }),
- );
-
- const defaultErrorMessage = s__('BulkImport|Importing the group failed');
- axios
- .post(endpoints.createBulkImport, {
- bulk_import: groups.map((group) => ({
- source_type: 'group_entity',
- source_full_path: group.full_path,
- destination_namespace: group.import_target.target_namespace,
- destination_name: group.import_target.new_name,
- })),
- })
- .then(({ data: { id: jobId } }) => {
- groupsManager.createImportState(jobId, {
- status: STATUSES.CREATED,
- groups,
- });
+ const progress = {
+ id: jobId,
+ status: STATUSES.CREATED,
+ };
- return { status: STATUSES.CREATED, jobId };
- })
- .catch((e) => {
- const message = e?.response?.data?.error ?? defaultErrorMessage;
- createFlash({ message });
- return { status: STATUSES.NONE };
- })
- .then((newStatus) =>
- sourceGroupIds.forEach((sourceGroupId, idx) =>
- client.mutate({
- mutation: setImportProgressMutation,
- variables: { sourceGroupId, ...newStatus, importTarget: groups[idx].import_target },
- }),
- ),
- )
- .catch(() => createFlash({ message: defaultErrorMessage }));
+ localStorageCache.set(op.group.webUrl, { progress, lastImportTarget });
- return GROUPS_BEING_SCHEDULED;
+ return makeGroup({ ...op.group, progress, lastImportTarget });
+ });
},
},
};
}
export const createApolloClient = ({ sourceUrl, endpoints }) =>
- createDefaultClient(
- createResolvers({ sourceUrl, endpoints }),
- { assumeImmutableResults: true },
- typeDefs,
- );
+ createDefaultClient(createResolvers({ sourceUrl, endpoints }), { typeDefs });
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
index 089340b3c48..0d83be7c0e8 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
@@ -2,22 +2,15 @@
fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup {
id
- web_url
- full_path
- full_name
+ webUrl
+ fullPath
+ fullName
+ lastImportTarget {
+ id
+ targetNamespace
+ newName
+ }
progress {
...BulkImportSourceGroupProgress
}
- import_target {
- target_namespace
- new_name
- }
- last_import_target {
- target_namespace
- new_name
- }
- validation_errors {
- field
- message
- }
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql
deleted file mode 100644
index d95c460c046..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-mutation addValidationError($sourceGroupId: String!, $field: String!, $message: String!) {
- addValidationError(sourceGroupId: $sourceGroupId, field: $field, message: $message) @client {
- id
- validation_errors {
- field
- message
- }
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
index d8e46329e38..75215471d0f 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
@@ -1,6 +1,11 @@
-mutation importGroups($sourceGroupIds: [String!]!) {
- importGroups(sourceGroupIds: $sourceGroupIds) @client {
+mutation importGroups($importRequests: [ImportGroupInput!]!) {
+ importGroups(importRequests: $importRequests) @client {
id
+ lastImportTarget {
+ id
+ targetNamespace
+ newName
+ }
progress {
id
status
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql
deleted file mode 100644
index 940bf4dfaac..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-mutation removeValidationError($sourceGroupId: String!, $field: String!) {
- removeValidationError(sourceGroupId: $sourceGroupId, field: $field) @client {
- id
- validation_errors {
- field
- message
- }
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql
deleted file mode 100644
index 43301554de3..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql
+++ /dev/null
@@ -1,23 +0,0 @@
-mutation setImportProgress(
- $status: String!
- $sourceGroupId: String!
- $jobId: String
- $importTarget: ImportTargetInput!
-) {
- setImportProgress(
- status: $status
- sourceGroupId: $sourceGroupId
- jobId: $jobId
- importTarget: $importTarget
- ) @client {
- id
- progress {
- id
- status
- }
- last_import_target {
- target_namespace
- new_name
- }
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql
deleted file mode 100644
index 793b60ee378..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql
+++ /dev/null
@@ -1,13 +0,0 @@
-mutation setImportTarget($newName: String!, $targetNamespace: String!, $sourceGroupId: String!) {
- setImportTarget(
- newName: $newName
- targetNamespace: $targetNamespace
- sourceGroupId: $sourceGroupId
- ) @client {
- id
- import_target {
- new_name
- target_namespace
- }
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
index 5ab9796b50a..b0741dfbe5c 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
@@ -1,6 +1,6 @@
query availableNamespaces {
availableNamespaces @client {
id
- full_path
+ fullPath
}
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql
deleted file mode 100644
index 0aff23af96d..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-#import "../fragments/bulk_import_source_group_item.fragment.graphql"
-
-query bulkImportSourceGroup($id: ID!) {
- bulkImportSourceGroup(id: $id) @client {
- ...BulkImportSourceGroupItem
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql
deleted file mode 100644
index d6124f84025..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-query groupAndProject($fullPath: ID!) {
- existingGroup: group(fullPath: $fullPath) {
- id
- }
-
- existingProject: project(fullPath: $fullPath) {
- id
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
new file mode 100644
index 00000000000..09bc7b33692
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
@@ -0,0 +1,74 @@
+import { debounce, merge } from 'lodash';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+const OLD_KEY = 'gl-bulk-imports-import-state';
+export const KEY = 'gl-bulk-imports-import-state-v2';
+export const DEBOUNCE_INTERVAL = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
+
+export class LocalStorageCache {
+ constructor({ storage = window.localStorage } = {}) {
+ this.storage = storage;
+ this.cache = this.loadCacheFromStorage();
+ try {
+ // remove old storage data
+ this.storage.removeItem(OLD_KEY);
+ } catch {
+ // empty catch intended
+ }
+
+ // cache for searching data by jobid
+ this.jobsLookupCache = {};
+ }
+
+ loadCacheFromStorage() {
+ try {
+ return JSON.parse(this.storage.getItem(KEY)) ?? {};
+ } catch {
+ return {};
+ }
+ }
+
+ set(webUrl, data) {
+ this.cache[webUrl] = data;
+ this.saveCacheToStorage();
+ // There are changes to jobIds, drop cache
+ this.jobsLookupCache = {};
+ }
+
+ get(webUrl) {
+ return this.cache[webUrl];
+ }
+
+ getCacheKeysByJobId(jobId) {
+ // this is invoked by polling, so we would like to cache results
+ if (!this.jobsLookupCache[jobId]) {
+ this.jobsLookupCache[jobId] = Object.keys(this.cache).filter(
+ (url) => this.cache[url]?.progress.id === jobId,
+ );
+ }
+
+ return this.jobsLookupCache[jobId];
+ }
+
+ updateStatusByJobId(jobId, status) {
+ this.getCacheKeysByJobId(jobId).forEach((webUrl) =>
+ this.set(webUrl, {
+ ...(this.get(webUrl) ?? {}),
+ progress: {
+ id: jobId,
+ status,
+ },
+ }),
+ );
+ this.saveCacheToStorage();
+ }
+
+ saveCacheToStorage = debounce(() => {
+ try {
+ // storage might be changed in other tab so fetch first
+ this.storage.setItem(KEY, JSON.stringify(merge({}, this.loadCacheFromStorage(), this.cache)));
+ } catch {
+ // empty catch intentional: storage might be unavailable or full
+ }
+ }, DEBOUNCE_INTERVAL);
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
deleted file mode 100644
index 7caa37d9ad4..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import { debounce, merge } from 'lodash';
-
-export const KEY = 'gl-bulk-imports-import-state';
-export const DEBOUNCE_INTERVAL = 200;
-
-export class SourceGroupsManager {
- constructor({ sourceUrl, storage = window.localStorage }) {
- this.sourceUrl = sourceUrl;
-
- this.storage = storage;
- this.importStates = this.loadImportStatesFromStorage();
- }
-
- loadImportStatesFromStorage() {
- try {
- return Object.fromEntries(
- Object.entries(JSON.parse(this.storage.getItem(KEY)) ?? {}).map(([jobId, config]) => {
- // new format of storage
- if (config.groups) {
- return [jobId, config];
- }
-
- return [
- jobId,
- {
- status: config.status,
- groups: [{ id: config.id, importTarget: config.importTarget }],
- },
- ];
- }),
- );
- } catch {
- return {};
- }
- }
-
- createImportState(importId, jobConfig) {
- this.importStates[importId] = {
- status: jobConfig.status,
- groups: jobConfig.groups.map((g) => ({
- importTarget: { ...g.import_target },
- id: g.id,
- })),
- };
- this.saveImportStatesToStorage();
- }
-
- updateImportProgress(importId, status) {
- const currentState = this.importStates[importId];
- if (!currentState) {
- return;
- }
-
- currentState.status = status;
- this.saveImportStatesToStorage();
- }
-
- getImportedGroupsByJobId(jobId) {
- return this.importStates[jobId]?.groups ?? [];
- }
-
- getImportStateFromStorageByGroupId(groupId) {
- const [jobId, importState] =
- Object.entries(this.importStates)
- .reverse()
- .find(([, state]) => state.groups.some((g) => g.id === groupId)) ?? [];
-
- if (!jobId) {
- return null;
- }
-
- const group = importState.groups.find((g) => g.id === groupId);
- return { jobId, importState: { ...group, status: importState.status } };
- }
-
- saveImportStatesToStorage = debounce(() => {
- try {
- // storage might be changed in other tab so fetch first
- this.storage.setItem(
- KEY,
- JSON.stringify(merge({}, this.loadImportStatesFromStorage(), this.importStates)),
- );
- } catch {
- // empty catch intentional: storage might be unavailable or full
- }
- }, DEBOUNCE_INTERVAL);
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
deleted file mode 100644
index 0297b3d3428..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import Poll from '~/lib/utils/poll';
-import { s__ } from '~/locale';
-
-export class StatusPoller {
- constructor({ updateImportStatus, pollPath }) {
- this.eTagPoll = new Poll({
- resource: {
- fetchJobs: () => axios.get(pollPath),
- },
- method: 'fetchJobs',
- successCallback: ({ data: statuses }) => {
- statuses.forEach((status) => updateImportStatus(status));
- },
- errorCallback: () =>
- createFlash({
- message: s__('BulkImport|Update of import statuses with realtime changes failed'),
- }),
- });
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- this.eTagPoll.restart();
- } else {
- this.eTagPoll.stop();
- }
- });
- }
-
- startPolling() {
- this.eTagPoll.makeRequest();
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
index 6ef4bbafec0..b8dd79a5000 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
@@ -1,11 +1,11 @@
type ClientBulkImportAvailableNamespace {
id: ID!
- full_path: String!
+ fullPath: String!
}
type ClientBulkImportTarget {
- target_namespace: String!
- new_name: String!
+ targetNamespace: String!
+ newName: String!
}
type ClientBulkImportSourceGroupConnection {
@@ -14,7 +14,7 @@ type ClientBulkImportSourceGroupConnection {
}
type ClientBulkImportProgress {
- id: ID
+ id: ID!
status: String!
}
@@ -25,13 +25,11 @@ type ClientBulkImportValidationError {
type ClientBulkImportSourceGroup {
id: ID!
- web_url: String!
- full_path: String!
- full_name: String!
- progress: ClientBulkImportProgress!
- import_target: ClientBulkImportTarget!
- last_import_target: ClientBulkImportTarget
- validation_errors: [ClientBulkImportValidationError!]!
+ webUrl: String!
+ fullPath: String!
+ fullName: String!
+ lastImportTarget: ClientBulkImportTarget
+ progress: ClientBulkImportProgress
}
type ClientBulkImportPageInfo {
@@ -41,8 +39,13 @@ type ClientBulkImportPageInfo {
totalPages: Int!
}
+type ClientBulkImportNamespaceSuggestion {
+ id: ID!
+ exists: Boolean!
+ suggestions: [String!]!
+}
+
extend type Query {
- bulkImportSourceGroup(id: ID!): ClientBulkImportSourceGroup
bulkImportSourceGroups(
page: Int!
perPage: Int!
@@ -51,26 +54,13 @@ extend type Query {
availableNamespaces: [ClientBulkImportAvailableNamespace!]!
}
-input InputTargetInput {
- target_namespace: String!
- new_name: String!
+input ImportRequestInput {
+ sourceGroupId: ID!
+ targetNamespace: String!
+ newName: String!
}
extend type Mutation {
- setNewName(newName: String, sourceGroupId: ID!): ClientBulkImportSourceGroup!
- setTargetNamespace(targetNamespace: String, sourceGroupId: ID!): ClientBulkImportSourceGroup!
- importGroups(sourceGroupIds: [ID!]!): [ClientBulkImportSourceGroup!]!
- setImportProgress(
- id: ID
- status: String!
- jobId: String
- importTarget: ImportTargetInput!
- ): ClientBulkImportSourceGroup!
- updateImportProgress(id: ID, status: String!): ClientBulkImportProgress
- addValidationError(
- sourceGroupId: ID!
- field: String!
- message: String!
- ): ClientBulkImportSourceGroup!
- removeValidationError(sourceGroupId: ID!, field: String!): ClientBulkImportSourceGroup!
+ importGroups(importRequests: [ImportRequestInput!]!): [ClientBulkImportSourceGroup!]!
+ updateImportStatus(id: ID, status: String!): ClientBulkImportProgress
}
diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js
index 07b839c5c82..67a7258d504 100644
--- a/app/assets/javascripts/import_entities/import_groups/index.js
+++ b/app/assets/javascripts/import_entities/import_groups/index.js
@@ -17,7 +17,6 @@ export function mountImportGroupsApp(mountElement) {
jobsPath,
sourceUrl,
groupPathRegex,
- groupUrlErrorMessage,
} = mountElement.dataset;
const apolloProvider = new VueApollo({
defaultClient: createApolloClient({
@@ -26,7 +25,6 @@ export function mountImportGroupsApp(mountElement) {
status: statusPath,
availableNamespaces: availableNamespacesPath,
createBulkImport: createBulkImportPath,
- jobs: jobsPath,
},
}),
});
@@ -38,8 +36,8 @@ export function mountImportGroupsApp(mountElement) {
return createElement(ImportTable, {
props: {
sourceUrl,
+ jobsPath,
groupPathRegex: new RegExp(`^(${groupPathRegex})$`),
- groupUrlErrorMessage,
},
});
},
diff --git a/app/assets/javascripts/import_entities/import_groups/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
new file mode 100644
index 00000000000..ba0f2bb947a
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
@@ -0,0 +1,39 @@
+import Visibility from 'visibilityjs';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
+import { s__ } from '~/locale';
+
+export class StatusPoller {
+ constructor({ updateImportStatus, pollPath }) {
+ this.eTagPoll = new Poll({
+ resource: {
+ fetchJobs: () => axios.get(pollPath),
+ },
+ method: 'fetchJobs',
+ successCallback: ({ data: statuses }) => {
+ statuses.forEach((status) => updateImportStatus(status));
+ },
+ errorCallback: () =>
+ createFlash({
+ message: s__('BulkImport|Update of import statuses with realtime changes failed'),
+ }),
+ });
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.eTagPoll.restart();
+ } else {
+ this.eTagPoll.stop();
+ }
+ });
+ }
+
+ startPolling() {
+ this.eTagPoll.makeRequest();
+ }
+
+ stopPolling() {
+ this.eTagPoll.stop();
+ }
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/utils.js b/app/assets/javascripts/import_entities/import_groups/utils.js
index a1baeaf39dd..1d0ab75e1cb 100644
--- a/app/assets/javascripts/import_entities/import_groups/utils.js
+++ b/app/assets/javascripts/import_entities/import_groups/utils.js
@@ -1,22 +1,25 @@
import { STATUSES } from '../constants';
import { NEW_NAME_FIELD } from './constants';
-export function isNameValid(group, validationRegex) {
- return validationRegex.test(group.import_target[NEW_NAME_FIELD]);
+export function isNameValid(importTarget, validationRegex) {
+ return validationRegex.test(importTarget[NEW_NAME_FIELD]);
}
-export function getInvalidNameValidationMessage(group) {
- return group.validation_errors.find(({ field }) => field === NEW_NAME_FIELD)?.message;
-}
-
-export function isInvalid(group, validationRegex) {
- return Boolean(!isNameValid(group, validationRegex) || getInvalidNameValidationMessage(group));
+export function getInvalidNameValidationMessage(importTarget) {
+ return importTarget.validationErrors?.find(({ field }) => field === NEW_NAME_FIELD)?.message;
}
export function isFinished(group) {
- return group.progress.status === STATUSES.FINISHED;
+ return [STATUSES.FINISHED, STATUSES.FAILED].includes(group.progress?.status);
}
export function isAvailableForImport(group) {
- return [STATUSES.NONE, STATUSES.FINISHED].some((status) => group.progress.status === status);
+ return !group.progress || isFinished(group);
+}
+
+export function isSameTarget(importTarget) {
+ return (target) =>
+ target !== importTarget &&
+ target.newName.toLowerCase() === importTarget.newName.toLowerCase() &&
+ target.targetNamespace.id === importTarget.targetNamespace.id;
}
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index 0cd3519bcec..b9f0b5012ac 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -46,10 +46,6 @@ export default {
return `${this.filter}-${this.repositories.length}-${this.pageInfo.page}`;
},
- availableNamespaces() {
- return this.namespaces.map(({ fullPath }) => fullPath);
- },
-
importAllButtonText() {
if (this.isImportingAnyRepo) {
return n__('Importing %d repository', 'Importing %d repositories', this.importingRepoCount);
@@ -167,7 +163,7 @@ export default {
<provider-repo-table-row
:key="repo.importSource.providerLink"
:repo="repo"
- :available-namespaces="availableNamespaces"
+ :available-namespaces="namespaces"
:user-namespace="defaultTargetNamespace"
/>
</template>
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index a97af5367fb..c3d0ca4ed8c 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -128,17 +128,17 @@ export default {
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="ns in namespaces"
- :key="ns"
+ :key="ns.fullPath"
data-qa-selector="target_group_dropdown_item"
- :data-qa-group-name="ns"
- @click="updateImportTarget({ targetNamespace: ns })"
+ :data-qa-group-name="ns.fullPath"
+ @click="updateImportTarget({ targetNamespace: ns.fullPath })"
>
- {{ ns }}
+ {{ ns.fullPath }}
</gl-dropdown-item>
<gl-dropdown-divider />
</template>
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
- <gl-dropdown-item @click="updateImportTarget({ targetNamespace: ns })">{{
+ <gl-dropdown-item @click="updateImportTarget({ targetNamespace: userNamespace })">{{
userNamespace
}}</gl-dropdown-item>
</import-group-dropdown>
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 4d34daa43ba..37597da3c8e 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -125,6 +125,7 @@ export default {
'authorUsernameQuery',
'assigneeUsernameQuery',
'slaFeatureAvailable',
+ 'canCreateIncident',
],
apollo: {
incidents: {
@@ -230,13 +231,16 @@ export default {
},
emptyStateData() {
const {
- emptyState: { title, emptyClosedTabTitle, description },
+ emptyState: { title, emptyClosedTabTitle, description, cannotCreateIncidentDescription },
createIncidentBtnLabel,
} = this.$options.i18n;
if (this.activeClosedTabHasNoIncidents) {
return { title: emptyClosedTabTitle };
}
+ if (!this.canCreateIncident) {
+ return { title, description: cannotCreateIncidentDescription };
+ }
return {
title,
description,
@@ -244,6 +248,9 @@ export default {
btnText: createIncidentBtnLabel,
};
},
+ isHeaderButtonVisible() {
+ return this.canCreateIncident && (!this.isEmpty || this.activeClosedTabHasNoIncidents);
+ },
},
methods: {
hasAssignees(assignees) {
@@ -311,7 +318,7 @@ export default {
>
<template #header-actions>
<gl-button
- v-if="!isEmpty || activeClosedTabHasNoIncidents"
+ v-if="isHeaderButtonVisible"
class="gl-my-3 gl-mr-5 create-incident-button"
data-testid="createIncidentBtn"
data-qa-selector="create_incident_button"
diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js
index b82980b5628..23909ae3b6c 100644
--- a/app/assets/javascripts/incidents/constants.js
+++ b/app/assets/javascripts/incidents/constants.js
@@ -11,7 +11,10 @@ export const I18N = {
title: s__('IncidentManagement|Display your incidents in a dedicated view'),
emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
description: s__(
- 'IncidentManagement|All alerts promoted to incidents will automatically be displayed within the list. You can also create a new incident using the button below.',
+ 'IncidentManagement|All alerts promoted to incidents are automatically displayed within the list. You can also create a new incident using the button below.',
+ ),
+ cannotCreateIncidentDescription: s__(
+ 'IncidentManagement|All alerts promoted to incidents are automatically displayed within the list.',
),
},
};
diff --git a/app/assets/javascripts/incidents/list.js b/app/assets/javascripts/incidents/list.js
index 6e6461cd7a9..1d40f1093a4 100644
--- a/app/assets/javascripts/incidents/list.js
+++ b/app/assets/javascripts/incidents/list.js
@@ -21,10 +21,11 @@ export default () => {
authorUsernameQuery,
assigneeUsernameQuery,
slaFeatureAvailable,
+ canCreateIncident,
} = domEl.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
return new Vue({
@@ -44,6 +45,7 @@ export default () => {
authorUsernameQuery,
assigneeUsernameQuery,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
+ canCreateIncident: parseBoolean(canCreateIncident),
},
apolloProvider,
render(createElement) {
diff --git a/app/assets/javascripts/init_confirm_danger.js b/app/assets/javascripts/init_confirm_danger.js
new file mode 100644
index 00000000000..d3d32c8be54
--- /dev/null
+++ b/app/assets/javascripts/init_confirm_danger.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import { parseBoolean } from './lib/utils/common_utils';
+import ConfirmDanger from './vue_shared/components/confirm_danger/confirm_danger.vue';
+
+export default () => {
+ const el = document.querySelector('.js-confirm-danger');
+ if (!el) return null;
+
+ const {
+ removeFormId = null,
+ phrase,
+ buttonText,
+ buttonTestid = null,
+ confirmDangerMessage,
+ disabled = false,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ confirmDangerMessage,
+ },
+ render: (createElement) =>
+ createElement(ConfirmDanger, {
+ props: {
+ phrase,
+ buttonText,
+ buttonTestid,
+ disabled: parseBoolean(disabled),
+ },
+ on: {
+ confirm: () => {
+ if (removeFormId) document.getElementById(removeFormId)?.submit();
+ },
+ },
+ }),
+ });
+};
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index 8a8d38b295c..d214ee4ded6 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
export const TEST_INTEGRATION_EVENT = 'testIntegration';
export const SAVE_INTEGRATION_EVENT = 'saveIntegration';
@@ -21,3 +21,9 @@ export const overrideDropdownDescriptions = {
'Integrations|Default settings are inherited from the instance level.',
),
};
+
+export const I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE = s__(
+ 'Integrations|Connection failed. Please check your settings.',
+);
+export const I18N_DEFAULT_ERROR_MESSAGE = __('Something went wrong on our end.');
+export const I18N_SUCCESSFUL_CONNECTION_MESSAGE = s__('Integrations|Connection successful.');
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index f30298676df..258cd1bf365 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -62,6 +62,14 @@ export default {
required: false,
default: null,
},
+ /**
+ * The label that is displayed inline with the checkbox.
+ */
+ checkboxLabel: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -152,7 +160,7 @@ export default {
<template v-if="isCheckbox">
<input :name="fieldName" type="hidden" :value="model || false" />
<gl-form-checkbox :id="fieldId" v-model="model" :disabled="isInheriting">
- {{ humanizedTitle }}
+ {{ checkboxLabel || humanizedTitle }}
</gl-form-checkbox>
</template>
<template v-else-if="isSelect">
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index 0521e1eeea5..7cbfb35aeaa 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -5,6 +5,7 @@ import {
VALIDATE_INTEGRATION_FORM_EVENT,
GET_JIRA_ISSUE_TYPES_EVENT,
} from '~/integrations/constants';
+import { s__, __ } from '~/locale';
import eventHub from '../event_hub';
import JiraUpgradeCta from './jira_upgrade_cta.vue';
@@ -94,33 +95,38 @@ export default {
eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
},
},
+ i18n: {
+ sectionTitle: s__('JiraService|View Jira issues in GitLab'),
+ sectionDescription: s__(
+ 'JiraService|Work on Jira issues without leaving GitLab. Adds a Jira menu to access your list of Jira issues and view any issue as read-only.',
+ ),
+ enableCheckboxLabel: s__('JiraService|Enable Jira issues'),
+ enableCheckboxHelp: s__(
+ 'JiraService|Warning: All GitLab users that have access to this GitLab project are able to view all issues from the Jira project specified below.',
+ ),
+ projectKeyLabel: s__('JiraService|Jira project key'),
+ projectKeyPlaceholder: s__('JiraService|For example, AB'),
+ requiredFieldFeedback: __('This field is required.'),
+ issueTrackerConflictWarning: s__(
+ 'JiraService|Displaying Jira issues while leaving the GitLab issue functionality enabled might be confusing. Consider %{linkStart}disabling GitLab issues%{linkEnd} if they won’t otherwise be used.',
+ ),
+ },
};
</script>
<template>
<div>
- <gl-form-group
- :label="s__('JiraService|View Jira issues in GitLab')"
- label-for="jira-issue-settings"
- >
+ <gl-form-group :label="$options.i18n.sectionTitle" label-for="jira-issue-settings">
<div id="jira-issue-settings">
<p>
- {{
- s__(
- 'JiraService|Work on Jira issues without leaving GitLab. Adds a Jira menu to access your list of Jira issues and view any issue as read-only.',
- )
- }}
+ {{ $options.i18n.sectionDescription }}
</p>
<template v-if="showJiraIssuesIntegration">
<input name="service[issues_enabled]" type="hidden" :value="enableJiraIssues || false" />
<gl-form-checkbox v-model="enableJiraIssues" :disabled="isInheriting">
- {{ s__('JiraService|Enable Jira issues') }}
+ {{ $options.i18n.enableCheckboxLabel }}
<template #help>
- {{
- s__(
- 'JiraService|Warning: All GitLab users that have access to this GitLab project are able to view all issues from the Jira project specified below.',
- )
- }}
+ {{ $options.i18n.enableCheckboxHelp }}
</template>
</gl-form-checkbox>
<template v-if="enableJiraIssues">
@@ -152,30 +158,25 @@ export default {
</gl-form-group>
<template v-if="showJiraIssuesIntegration">
<gl-form-group
- :label="s__('JiraService|Jira project key')"
+ :label="$options.i18n.projectKeyLabel"
label-for="service_project_key"
- :invalid-feedback="__('This field is required.')"
+ :invalid-feedback="$options.i18n.requiredFieldFeedback"
:state="validProjectKey"
+ data-testid="project-key-form-group"
>
<gl-form-input
id="service_project_key"
v-model="projectKey"
name="service[project_key]"
- :placeholder="s__('JiraService|For example, AB')"
+ :placeholder="$options.i18n.projectKeyPlaceholder"
:required="enableJiraIssues"
:state="validProjectKey"
:disabled="!enableJiraIssues"
:readonly="isInheriting"
/>
</gl-form-group>
- <p v-if="gitlabIssuesEnabled">
- <gl-sprintf
- :message="
- s__(
- 'JiraService|Displaying Jira issues while leaving the GitLab issue functionality enabled might be confusing. Consider %{linkStart}disabling GitLab issues%{linkEnd} if they won’t otherwise be used.',
- )
- "
- >
+ <p v-if="gitlabIssuesEnabled" data-testid="conflict-warning-text">
+ <gl-sprintf :message="$options.i18n.issueTrackerConflictWarning">
<template #link="{ content }">
<gl-link :href="editProjectPath" target="_blank">{{ content }}</gl-link>
</template>
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index f33364d5545..f519fc87c46 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,5 +1,4 @@
import { delay } from 'lodash';
-import { __, s__ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import axios from '../lib/utils/axios_utils';
import initForm from './edit';
@@ -10,6 +9,9 @@ import {
GET_JIRA_ISSUE_TYPES_EVENT,
TOGGLE_INTEGRATION_EVENT,
VALIDATE_INTEGRATION_FORM_EVENT,
+ I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
+ I18N_DEFAULT_ERROR_MESSAGE,
+ I18N_SUCCESSFUL_CONNECTION_MESSAGE,
} from './constants';
export default class IntegrationSettingsForm {
@@ -104,11 +106,7 @@ export default class IntegrationSettingsForm {
return this.fetchTestSettings(formData)
.then(
({
- data: {
- issuetypes,
- error,
- message = s__('Integrations|Connection failed. Please check your settings.'),
- },
+ data: { issuetypes, error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE },
}) => {
if (error || !issuetypes?.length) {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
@@ -118,7 +116,7 @@ export default class IntegrationSettingsForm {
dispatch('receiveJiraIssueTypesSuccess', issuetypes);
},
)
- .catch(({ message = __('Something went wrong on our end.') }) => {
+ .catch(({ message = I18N_DEFAULT_ERROR_MESSAGE }) => {
dispatch('receiveJiraIssueTypesError', message);
});
}
@@ -140,11 +138,11 @@ export default class IntegrationSettingsForm {
toast(`${data.message} ${data.service_response}`);
} else {
this.vue.$store.dispatch('receiveJiraIssueTypesSuccess', data.issuetypes);
- toast(s__('Integrations|Connection successful.'));
+ toast(I18N_SUCCESSFUL_CONNECTION_MESSAGE);
}
})
.catch(() => {
- toast(__('Something went wrong on our end.'));
+ toast(I18N_DEFAULT_ERROR_MESSAGE);
})
.finally(() => {
this.vue.$store.dispatch('setIsTesting', false);
diff --git a/app/assets/javascripts/invite_members/components/confetti.vue b/app/assets/javascripts/invite_members/components/confetti.vue
new file mode 100644
index 00000000000..2e5744afcd4
--- /dev/null
+++ b/app/assets/javascripts/invite_members/components/confetti.vue
@@ -0,0 +1,33 @@
+<script>
+import confetti from 'canvas-confetti';
+
+export default {
+ mounted() {
+ confetti.create(this.$refs.canvas, {
+ resize: true,
+ useWorker: true,
+ disableForReducedMotion: true,
+ });
+
+ this.basicCannon();
+ },
+ methods: {
+ basicCannon() {
+ confetti({
+ particleCount: 100,
+ spread: 70,
+ origin: { y: 0.2 },
+ scalar: 2,
+ shapes: ['square'],
+ colors: ['#FC6D26', '#6B4FBB', '#FDB997'],
+ zIndex: 1045,
+ gravity: 1.5,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <canvas ref="canvas" width="0" height="0"></canvas>
+</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
index cd0b413265b..cf4f434a7a8 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -1,5 +1,6 @@
<script>
import {
+ GlAlert,
GlFormGroup,
GlModal,
GlDropdown,
@@ -11,29 +12,34 @@ import {
GlFormInput,
GlFormCheckboxGroup,
} from '@gitlab/ui';
-import { partition, isString, unescape } from 'lodash';
+import { partition, isString, unescape, uniqueId } from 'lodash';
import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { sanitize } from '~/lib/dompurify';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
-import { s__, sprintf } from '~/locale';
+import { getParameterValues } from '~/lib/utils/url_utility';
+import { sprintf } from '~/locale';
import {
INVITE_MEMBERS_IN_COMMENT,
GROUP_FILTERS,
USERS_FILTER_ALL,
MEMBER_AREAS_OF_FOCUS,
+ INVITE_MEMBERS_FOR_TASK,
+ MODAL_LABELS,
} from '../constants';
import eventHub from '../event_hub';
import {
responseMessageFromError,
responseMessageFromSuccess,
} from '../utils/response_message_parser';
+import ModalConfetti from './confetti.vue';
import GroupSelect from './group_select.vue';
import MembersTokenSelect from './members_token_select.vue';
export default {
name: 'InviteMembersModal',
components: {
+ GlAlert,
GlFormGroup,
GlDatepicker,
GlLink,
@@ -46,7 +52,9 @@ export default {
GlFormCheckboxGroup,
MembersTokenSelect,
GroupSelect,
+ ModalConfetti,
},
+ inject: ['newProjectPath'],
props: {
id: {
type: String,
@@ -100,36 +108,54 @@ export default {
type: Array,
required: true,
},
+ tasksToBeDoneOptions: {
+ type: Array,
+ required: true,
+ },
+ projects: {
+ type: Array,
+ required: true,
+ },
},
data() {
return {
visible: true,
- modalId: 'invite-members-modal',
+ modalId: uniqueId('invite-members-modal-'),
selectedAccessLevel: this.defaultAccessLevel,
inviteeType: 'members',
newUsersToInvite: [],
selectedDate: undefined,
selectedAreasOfFocus: [],
+ selectedTasksToBeDone: [],
+ selectedTaskProject: this.projects[0],
groupToBeSharedWith: {},
source: 'unknown',
invalidFeedbackMessage: '',
isLoading: false,
+ mode: 'default',
};
},
computed: {
+ isCelebration() {
+ return this.mode === 'celebrate';
+ },
validationState() {
return this.invalidFeedbackMessage === '' ? null : false;
},
isInviteGroup() {
return this.inviteeType === 'group';
},
+ modalTitle() {
+ return this.$options.labels[this.inviteeType].modal[this.mode].title;
+ },
introText() {
- const inviteTo = this.isProject ? 'toProject' : 'toGroup';
-
- return sprintf(this.$options.labels[this.inviteeType][inviteTo].introText, {
+ return sprintf(this.$options.labels[this.inviteeType][this.inviteTo][this.mode].introText, {
name: this.name,
});
},
+ inviteTo() {
+ return this.isProject ? 'toProject' : 'toGroup';
+ },
toastOptions() {
return {
onComplete: () => {
@@ -156,7 +182,7 @@ export default {
);
},
areasOfFocusEnabled() {
- return this.areasOfFocusOptions.length !== 0;
+ return !this.tasksToBeDoneEnabled && this.areasOfFocusOptions.length !== 0;
},
areasOfFocusForPost() {
if (this.selectedAreasOfFocus.length === 0 && this.areasOfFocusEnabled) {
@@ -172,12 +198,40 @@ export default {
return this.$options.labels[this.inviteeType].placeHolder;
},
+ tasksToBeDoneEnabled() {
+ return (
+ getParameterValues('open_modal')[0] === 'invite_members_for_task' &&
+ this.tasksToBeDoneOptions.length
+ );
+ },
+ showTasksToBeDone() {
+ return (
+ this.tasksToBeDoneEnabled &&
+ this.selectedAccessLevel >= INVITE_MEMBERS_FOR_TASK.minimum_access_level
+ );
+ },
+ showTaskProjects() {
+ return !this.isProject && this.selectedTasksToBeDone.length;
+ },
+ tasksToBeDoneForPost() {
+ return this.showTasksToBeDone ? this.selectedTasksToBeDone : [];
+ },
+ tasksProjectForPost() {
+ return this.showTasksToBeDone && this.selectedTasksToBeDone.length
+ ? this.selectedTaskProject.id
+ : '';
+ },
},
mounted() {
eventHub.$on('openModal', (options) => {
this.openModal(options);
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.view);
});
+
+ if (this.tasksToBeDoneEnabled) {
+ this.openModal({ inviteeType: 'members', source: 'in_product_marketing_email' });
+ this.trackEvent(INVITE_MEMBERS_FOR_TASK.name, INVITE_MEMBERS_FOR_TASK.view);
+ }
},
methods: {
partitionNewUsersToInvite() {
@@ -191,7 +245,8 @@ export default {
usersToAddById.map((user) => user.id).join(','),
];
},
- openModal({ inviteeType, source }) {
+ openModal({ mode = 'default', inviteeType, source }) {
+ this.mode = mode;
this.inviteeType = inviteeType;
this.source = source;
@@ -219,6 +274,12 @@ export default {
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.submit);
},
+ trackinviteMembersForTask() {
+ const label = 'selected_tasks_to_be_done';
+ const property = this.selectedTasksToBeDone.join(',');
+ const tracking = new ExperimentTracking(INVITE_MEMBERS_FOR_TASK.name, { label, property });
+ tracking.event(INVITE_MEMBERS_FOR_TASK.submit);
+ },
resetFields() {
this.isLoading = false;
this.selectedAccessLevel = this.defaultAccessLevel;
@@ -227,10 +288,15 @@ export default {
this.groupToBeSharedWith = {};
this.invalidFeedbackMessage = '';
this.selectedAreasOfFocus = [];
+ this.selectedTasksToBeDone = [];
+ [this.selectedTaskProject] = this.projects;
},
changeSelectedItem(item) {
this.selectedAccessLevel = item;
},
+ changeSelectedTaskProject(project) {
+ this.selectedTaskProject = project;
+ },
submitShareWithGroup() {
const apiShareWithGroup = this.isProject
? Api.projectShareWithGroup.bind(Api)
@@ -263,6 +329,7 @@ export default {
promises.push(apiAddByUserId(this.id, this.addByUserIdPostData(usersToAddById)));
}
this.trackInvite();
+ this.trackinviteMembersForTask();
Promise.all(promises)
.then(this.conditionallyShowToastSuccess)
@@ -275,6 +342,8 @@ export default {
access_level: this.selectedAccessLevel,
invite_source: this.source,
areas_of_focus: this.areasOfFocusForPost,
+ tasks_to_be_done: this.tasksToBeDoneForPost,
+ tasks_project_id: this.tasksProjectForPost,
};
},
addByUserIdPostData(usersToAddById) {
@@ -284,6 +353,8 @@ export default {
access_level: this.selectedAccessLevel,
invite_source: this.source,
areas_of_focus: this.areasOfFocusForPost,
+ tasks_to_be_done: this.tasksToBeDoneForPost,
+ tasks_project_id: this.tasksProjectForPost,
};
},
shareWithGroupPostData(groupToBeSharedWith) {
@@ -322,49 +393,7 @@ export default {
return unescape(sanitize(message, { ALLOWED_TAGS: [] }));
},
},
- labels: {
- members: {
- modalTitle: s__('InviteMembersModal|Invite members'),
- searchField: s__('InviteMembersModal|GitLab member or email address'),
- placeHolder: s__('InviteMembersModal|Select members or type email addresses'),
- toGroup: {
- introText: s__(
- "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} group.",
- ),
- },
- toProject: {
- introText: s__(
- "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} project.",
- ),
- },
- },
- group: {
- modalTitle: s__('InviteMembersModal|Invite a group'),
- searchField: s__('InviteMembersModal|Select a group to invite'),
- placeHolder: s__('InviteMembersModal|Search for a group to invite'),
- toGroup: {
- introText: s__(
- "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group.",
- ),
- },
- toProject: {
- introText: s__(
- "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
- ),
- },
- },
- accessLevel: s__('InviteMembersModal|Select a role'),
- accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'),
- toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'),
- invalidFeedbackMessageDefault: s__('InviteMembersModal|Something went wrong'),
- readMoreText: s__(`InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`),
- inviteButtonText: s__('InviteMembersModal|Invite'),
- cancelButtonText: s__('InviteMembersModal|Cancel'),
- headerCloseLabel: s__('InviteMembersModal|Close invite team members'),
- areasOfFocusLabel: s__(
- 'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
- ),
- },
+ labels: MODAL_LABELS,
membersTokenSelectLabelId: 'invite-members-input',
};
</script>
@@ -374,20 +403,29 @@ export default {
:modal-id="modalId"
size="sm"
data-qa-selector="invite_members_modal_content"
- :title="$options.labels[inviteeType].modalTitle"
+ data-testid="invite-members-modal"
+ :title="modalTitle"
:header-close-label="$options.labels.headerCloseLabel"
@hidden="resetFields"
@close="resetFields"
@hide="resetFields"
>
<div>
- <p ref="introText">
- <gl-sprintf :message="introText">
- <template #strong="{ content }">
- <strong>{{ content }}</strong>
- </template>
- </gl-sprintf>
- </p>
+ <div class="gl-display-flex">
+ <div v-if="isCelebration" class="gl-p-4 gl-font-size-h1"><gl-emoji data-name="tada" /></div>
+ <div>
+ <p ref="introText">
+ <gl-sprintf :message="introText">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ <br />
+ <span v-if="isCelebration">{{ $options.labels.members.modal.celebrate.intro }} </span>
+ <modal-confetti v-if="isCelebration" />
+ </p>
+ </div>
+ </div>
<gl-form-group
:invalid-feedback="invalidFeedbackMessage"
@@ -476,24 +514,70 @@ export default {
data-testid="area-of-focus-checks"
/>
</div>
+ <div v-if="showTasksToBeDone" data-testid="invite-members-modal-tasks-to-be-done">
+ <label class="gl-mt-5">
+ {{ $options.labels.members.tasksToBeDone.title }}
+ </label>
+ <template v-if="projects.length">
+ <gl-form-checkbox-group
+ v-model="selectedTasksToBeDone"
+ :options="tasksToBeDoneOptions"
+ data-testid="invite-members-modal-tasks"
+ />
+ <template v-if="showTaskProjects">
+ <label class="gl-mt-5 gl-display-block">
+ {{ $options.labels.members.tasksProject.title }}
+ </label>
+ <gl-dropdown
+ class="gl-w-half gl-xs-w-full"
+ :text="selectedTaskProject.title"
+ data-testid="invite-members-modal-project-select"
+ >
+ <template v-for="project in projects">
+ <gl-dropdown-item
+ :key="project.id"
+ active-class="is-active"
+ is-check-item
+ :is-checked="project.id === selectedTaskProject.id"
+ @click="changeSelectedTaskProject(project)"
+ >
+ {{ project.title }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+ </template>
+ </template>
+ <gl-alert
+ v-else-if="tasksToBeDoneEnabled"
+ variant="tip"
+ :dismissible="false"
+ data-testid="invite-members-modal-no-projects-alert"
+ >
+ <gl-sprintf :message="$options.labels.members.tasksToBeDone.noProjects">
+ <template #link="{ content }">
+ <gl-link :href="newProjectPath" target="_blank" class="gl-label-link">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+ </div>
</div>
<template #modal-footer>
- <div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0">
- <gl-button data-testid="cancel-button" @click="closeModal">
- {{ $options.labels.cancelButtonText }}
- </gl-button>
- <div class="gl-mr-3"></div>
- <gl-button
- :disabled="inviteDisabled"
- :loading="isLoading"
- variant="success"
- data-qa-selector="invite_button"
- data-testid="invite-button"
- @click="sendInvite"
- >{{ $options.labels.inviteButtonText }}</gl-button
- >
- </div>
+ <gl-button data-testid="cancel-button" @click="closeModal">
+ {{ $options.labels.cancelButtonText }}
+ </gl-button>
+ <gl-button
+ :disabled="inviteDisabled"
+ :loading="isLoading"
+ variant="success"
+ data-qa-selector="invite_button"
+ data-testid="invite-button"
+ @click="sendInvite"
+ >
+ {{ $options.labels.inviteButtonText }}
+ </gl-button>
</template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
index 05be427742c..bf3250f63a5 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -1,11 +1,12 @@
<script>
-import { GlButton, GlLink } from '@gitlab/ui';
+import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
+import { TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_SIDE_NAV } from '../constants';
export default {
- components: { GlButton, GlLink },
+ components: { GlButton, GlLink, GlIcon },
props: {
displayText: {
type: String,
@@ -53,13 +54,11 @@ export default {
},
},
computed: {
- isButton() {
- return this.triggerElement === 'button';
- },
componentAttributes() {
const baseAttributes = {
class: this.classes,
'data-qa-selector': 'invite_members_button',
+ 'data-test-id': 'invite-members-button',
};
if (this.event && this.label) {
@@ -77,6 +76,9 @@ export default {
this.trackExperimentOnShow();
},
methods: {
+ checkTrigger(targetTriggerElement) {
+ return this.triggerElement === targetTriggerElement;
+ },
openModal() {
eventHub.$emit('openModal', { inviteeType: 'members', source: this.triggerSource });
},
@@ -87,12 +89,14 @@ export default {
}
},
},
+ TRIGGER_ELEMENT_BUTTON,
+ TRIGGER_ELEMENT_SIDE_NAV,
};
</script>
<template>
<gl-button
- v-if="isButton"
+ v-if="checkTrigger($options.TRIGGER_ELEMENT_BUTTON)"
v-bind="componentAttributes"
:variant="variant"
:icon="icon"
@@ -100,6 +104,17 @@ export default {
>
{{ displayText }}
</gl-button>
+ <gl-link
+ v-else-if="checkTrigger($options.TRIGGER_ELEMENT_SIDE_NAV)"
+ v-bind="componentAttributes"
+ data-is-link="true"
+ @click="openModal"
+ >
+ <span class="nav-icon-container">
+ <gl-icon :name="icon" />
+ </span>
+ <span class="nav-item-name"> {{ displayText }} </span>
+ </gl-link>
<gl-link v-else v-bind="componentAttributes" data-is-link="true" @click="openModal">
{{ displayText }}
</gl-link>
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index d7daf83e26b..59d4c2f3077 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -1,4 +1,4 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const SEARCH_DELAY = 200;
@@ -8,6 +8,12 @@ export const MEMBER_AREAS_OF_FOCUS = {
view: 'view',
submit: 'submit',
};
+export const INVITE_MEMBERS_FOR_TASK = {
+ minimum_access_level: 30,
+ name: 'invite_members_for_task',
+ view: 'modal_opened_from_email',
+ submit: 'submit',
+};
export const GROUP_FILTERS = {
ALL: 'all',
@@ -19,3 +25,122 @@ export const API_MESSAGES = {
};
export const USERS_FILTER_ALL = 'all';
export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id';
+export const TRIGGER_ELEMENT_BUTTON = 'button';
+export const TRIGGER_ELEMENT_SIDE_NAV = 'side-nav';
+export const MEMBERS_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite members');
+export const MEMBERS_MODAL_CELEBRATE_TITLE = s__(
+ 'InviteMembersModal|GitLab is better with colleagues!',
+);
+export const MEMBERS_MODAL_CELEBRATE_INTRO = s__(
+ 'InviteMembersModal|How about inviting a colleague or two to join you?',
+);
+export const MEMBERS_TO_GROUP_DEFAULT_INTRO_TEXT = s__(
+ "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} group.",
+);
+
+export const MEMBERS_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
+ "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} project.",
+);
+export const MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT = s__(
+ "InviteMembersModal|Congratulations on creating your project, you're almost there!",
+);
+export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|GitLab member or email address');
+export const MEMBERS_PLACEHOLDER = s__('InviteMembersModal|Select members or type email addresses');
+export const MEMBERS_TASKS_TO_BE_DONE_TITLE = s__(
+ 'InviteMembersModal|Create issues for your new team member to work on (optional)',
+);
+export const MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS = s__(
+ 'InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}',
+);
+export const MEMBERS_TASKS_PROJECTS_TITLE = s__(
+ 'InviteMembersModal|Choose a project for the issues',
+);
+
+export const GROUP_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite a group');
+export const GROUP_MODAL_TO_GROUP_DEFAULT_INTRO_TEXT = s__(
+ "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group.",
+);
+export const GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
+ "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
+);
+
+export const GROUP_SEARCH_FIELD = s__('InviteMembersModal|Select a group to invite');
+export const GROUP_PLACEHOLDER = s__('InviteMembersModal|Search for a group to invite');
+
+export const ACCESS_LEVEL = s__('InviteMembersModal|Select a role');
+export const ACCESS_EXPIRE_DATE = s__('InviteMembersModal|Access expiration date (optional)');
+export const TOAST_MESSAGE_SUCCESSFUL = s__('InviteMembersModal|Members were successfully added');
+export const INVALID_FEEDBACK_MESSAGE_DEFAULT = s__('InviteMembersModal|Something went wrong');
+export const READ_MORE_TEXT = s__(
+ `InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`,
+);
+export const INVITE_BUTTON_TEXT = s__('InviteMembersModal|Invite');
+export const CANCEL_BUTTON_TEXT = s__('InviteMembersModal|Cancel');
+export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team members');
+export const AREAS_OF_FOCUS_LABEL = s__(
+ 'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
+);
+
+export const MODAL_LABELS = {
+ members: {
+ modal: {
+ default: {
+ title: MEMBERS_MODAL_DEFAULT_TITLE,
+ },
+ celebrate: {
+ title: MEMBERS_MODAL_CELEBRATE_TITLE,
+ intro: MEMBERS_MODAL_CELEBRATE_INTRO,
+ },
+ },
+ toGroup: {
+ default: {
+ introText: MEMBERS_TO_GROUP_DEFAULT_INTRO_TEXT,
+ },
+ },
+ toProject: {
+ default: {
+ introText: MEMBERS_TO_PROJECT_DEFAULT_INTRO_TEXT,
+ },
+ celebrate: {
+ introText: MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
+ },
+ },
+ searchField: MEMBERS_SEARCH_FIELD,
+ placeHolder: MEMBERS_PLACEHOLDER,
+ tasksToBeDone: {
+ title: MEMBERS_TASKS_TO_BE_DONE_TITLE,
+ noProjects: MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS,
+ },
+ tasksProject: {
+ title: MEMBERS_TASKS_PROJECTS_TITLE,
+ },
+ },
+ group: {
+ modal: {
+ default: {
+ title: GROUP_MODAL_DEFAULT_TITLE,
+ },
+ },
+ toGroup: {
+ default: {
+ introText: GROUP_MODAL_TO_GROUP_DEFAULT_INTRO_TEXT,
+ },
+ },
+ toProject: {
+ default: {
+ introText: GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT,
+ },
+ },
+ searchField: GROUP_SEARCH_FIELD,
+ placeHolder: GROUP_PLACEHOLDER,
+ },
+ accessLevel: ACCESS_LEVEL,
+ accessExpireDate: ACCESS_EXPIRE_DATE,
+ toastMessageSuccessful: TOAST_MESSAGE_SUCCESSFUL,
+ invalidFeedbackMessageDefault: INVALID_FEEDBACK_MESSAGE_DEFAULT,
+ readMoreText: READ_MORE_TEXT,
+ inviteButtonText: INVITE_BUTTON_TEXT,
+ cancelButtonText: CANCEL_BUTTON_TEXT,
+ headerCloseLabel: HEADER_CLOSE_LABEL,
+ areasOfFocusLabel: AREAS_OF_FOCUS_LABEL,
+};
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index c1dfaa25dc7..fc657a064dd 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -5,15 +5,32 @@ import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(GlToast);
+let initedInviteMembersModal;
+
export default function initInviteMembersModal() {
+ if (initedInviteMembersModal) {
+ // if we already loaded this in another part of the dom, we don't want to do it again
+ // else we will stack the modals
+ return false;
+ }
+
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/344955
+ // bug lying in wait here for someone to put group and project invite in same screen
+ // once that happens we'll need to mount these differently, perhaps split
+ // group/project to each mount one, with many ways to open it.
const el = document.querySelector('.js-invite-members-modal');
if (!el) {
return false;
}
+ initedInviteMembersModal = true;
+
return new Vue({
el,
+ provide: {
+ newProjectPath: el.dataset.newProjectPath,
+ },
render: (createElement) =>
createElement(InviteMembersModal, {
props: {
@@ -24,6 +41,8 @@ export default function initInviteMembersModal() {
groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10),
areasOfFocusOptions: JSON.parse(el.dataset.areasOfFocusOptions),
+ tasksToBeDoneOptions: JSON.parse(el.dataset.tasksToBeDoneOptions || '[]'),
+ projects: JSON.parse(el.dataset.projects || '[]'),
noSelectionAreasOfFocus: JSON.parse(el.dataset.noSelectionAreasOfFocus),
usersFilter: el.dataset.usersFilter,
filterId: parseInt(el.dataset.filterId, 10),
diff --git a/app/assets/javascripts/issuable/components/issuable_by_email.vue b/app/assets/javascripts/issuable/components/issuable_by_email.vue
index 6e300831e00..799d2bdc9e2 100644
--- a/app/assets/javascripts/issuable/components/issuable_by_email.vue
+++ b/app/assets/javascripts/issuable/components/issuable_by_email.vue
@@ -166,7 +166,7 @@ export default {
</gl-sprintf>
</p>
<template #modal-footer>
- <gl-button category="secondary" @click="cancelHandler">{{ s__('Cancel') }}</gl-button>
+ <gl-button category="secondary" @click="cancelHandler">{{ __('Cancel') }}</gl-button>
</template>
</gl-modal>
</div>
diff --git a/app/assets/javascripts/issuable_suggestions/index.js b/app/assets/javascripts/issuable_suggestions/index.js
index 22a99a17741..8f7f317d6b4 100644
--- a/app/assets/javascripts/issuable_suggestions/index.js
+++ b/app/assets/javascripts/issuable_suggestions/index.js
@@ -10,12 +10,7 @@ export default function initIssuableSuggestions() {
const issueTitle = document.getElementById('issue_title');
const { projectPath } = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index 2c9a512acdb..d3b58ed3012 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -4,7 +4,7 @@ import Visibility from 'visibilityjs';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import { visitUrl } from '~/lib/utils/url_utility';
-import { __, s__, sprintf } from '~/locale';
+import { __, sprintf } from '~/locale';
import {
IssuableStatus,
IssuableStatusText,
@@ -12,6 +12,7 @@ import {
IssueTypePath,
IncidentTypePath,
IncidentType,
+ POLLING_DELAY,
} from '../constants';
import eventHub from '../event_hub';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
@@ -249,7 +250,7 @@ export default {
return false;
},
defaultErrorMessage() {
- return sprintf(s__('Error updating %{issuableType}'), { issuableType: this.issuableType });
+ return sprintf(__('Error updating %{issuableType}'), { issuableType: this.issuableType });
},
isClosed() {
return this.issuableStatus === IssuableStatus.Closed;
@@ -282,7 +283,7 @@ export default {
});
if (!Visibility.hidden()) {
- this.poll.makeDelayedRequest(2000);
+ this.poll.makeDelayedRequest(POLLING_DELAY);
}
Visibility.change(() => {
@@ -436,7 +437,7 @@ export default {
})
.catch(() => {
createFlash({
- message: sprintf(s__('Error deleting %{issuableType}'), {
+ message: sprintf(__('Error deleting %{issuableType}'), {
issuableType: this.issuableType,
}),
});
@@ -457,6 +458,22 @@ export default {
this.flashContainer = null;
}
},
+
+ taskListUpdateStarted() {
+ this.poll.stop();
+ },
+
+ taskListUpdateSucceeded() {
+ this.poll.enable();
+ this.poll.makeDelayedRequest(POLLING_DELAY);
+ },
+
+ taskListUpdateFailed() {
+ this.poll.enable();
+ this.poll.makeDelayedRequest(POLLING_DELAY);
+
+ this.updateStoreState();
+ },
},
};
</script>
@@ -552,7 +569,9 @@ export default {
:issuable-type="issuableType"
:update-url="updateEndpoint"
:lock-version="state.lock_version"
- @taskListUpdateFailed="updateStoreState"
+ @taskListUpdateStarted="taskListUpdateStarted"
+ @taskListUpdateSucceeded="taskListUpdateSucceeded"
+ @taskListUpdateFailed="taskListUpdateFailed"
/>
<edited-component
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 4c6a1478e95..9dc122d426c 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -2,7 +2,7 @@
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import $ from 'jquery';
import createFlash from '~/flash';
-import { s__, sprintf } from '~/locale';
+import { __, sprintf } from '~/locale';
import TaskList from '../../task_list';
import animateMixin from '../mixins/animate';
@@ -86,15 +86,25 @@ export default {
fieldName: 'description',
lockVersion: this.lockVersion,
selector: '.detail-page-description',
+ onUpdate: this.taskListUpdateStarted.bind(this),
+ onSuccess: this.taskListUpdateSuccess.bind(this),
onError: this.taskListUpdateError.bind(this),
});
}
},
+ taskListUpdateStarted() {
+ this.$emit('taskListUpdateStarted');
+ },
+
+ taskListUpdateSuccess() {
+ this.$emit('taskListUpdateSucceeded');
+ },
+
taskListUpdateError() {
createFlash({
message: sprintf(
- s__(
+ __(
'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.',
),
{
diff --git a/app/assets/javascripts/issue_show/components/fields/type.vue b/app/assets/javascripts/issue_show/components/fields/type.vue
index 3eac448c637..9110a6924b4 100644
--- a/app/assets/javascripts/issue_show/components/fields/type.vue
+++ b/app/assets/javascripts/issue_show/components/fields/type.vue
@@ -2,7 +2,7 @@
import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { capitalize } from 'lodash';
import { __ } from '~/locale';
-import { IssuableTypes } from '../../constants';
+import { IssuableTypes, IncidentType } from '../../constants';
import getIssueStateQuery from '../../queries/get_issue_state.query.graphql';
import updateIssueStateMutation from '../../queries/update_issue_state.mutation.graphql';
@@ -19,6 +19,14 @@ export default {
GlDropdown,
GlDropdownItem,
},
+ inject: {
+ canCreateIncident: {
+ default: false,
+ },
+ issueType: {
+ default: 'issue',
+ },
+ },
data() {
return {
issueState: {},
@@ -36,6 +44,9 @@ export default {
} = this;
return capitalize(issueType);
},
+ shouldShowIncident() {
+ return this.issueType === IncidentType || this.canCreateIncident;
+ },
},
methods: {
updateIssueType(issueType) {
@@ -47,6 +58,9 @@ export default {
},
});
},
+ isShown(type) {
+ return type.value !== IncidentType || this.shouldShowIncident;
+ },
},
};
</script>
@@ -68,6 +82,7 @@ export default {
>
<gl-dropdown-item
v-for="type in $options.IssuableTypes"
+ v-show="isShown(type)"
:key="type.value"
:is-checked="issueState.issueType === type.value"
is-check-item
diff --git a/app/assets/javascripts/issue_show/components/header_actions.vue b/app/assets/javascripts/issue_show/components/header_actions.vue
index 2bddbe4faa0..2c314ce1c3f 100644
--- a/app/assets/javascripts/issue_show/components/header_actions.vue
+++ b/app/assets/javascripts/issue_show/components/header_actions.vue
@@ -192,9 +192,14 @@ export default {
class="gl-sm-display-none! w-100"
block
:text="dropdownText"
+ data-qa-selector="issue_actions_dropdown"
:loading="isToggleStateButtonLoading"
>
- <gl-dropdown-item v-if="showToggleIssueStateButton" @click="toggleIssueState">
+ <gl-dropdown-item
+ v-if="showToggleIssueStateButton"
+ :data-qa-selector="`mobile_${qaSelector}`"
+ @click="toggleIssueState"
+ >
{{ buttonText }}
</gl-dropdown-item>
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
diff --git a/app/assets/javascripts/issue_show/constants.js b/app/assets/javascripts/issue_show/constants.js
index 64d39a79821..ef9699deb42 100644
--- a/app/assets/javascripts/issue_show/constants.js
+++ b/app/assets/javascripts/issue_show/constants.js
@@ -37,3 +37,10 @@ export const IncidentTypePath = 'issues/incident';
export const IncidentType = 'incident';
export const issueState = { issueType: undefined, isDirty: false };
+
+export const POLLING_DELAY = 2000;
+
+export const WorkspaceType = {
+ project: 'project',
+ group: 'group',
+};
diff --git a/app/assets/javascripts/issue_show/incident.js b/app/assets/javascripts/issue_show/incident.js
index df986195656..3aff2d9c54a 100644
--- a/app/assets/javascripts/issue_show/incident.js
+++ b/app/assets/javascripts/issue_show/incident.js
@@ -2,25 +2,31 @@ import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import issuableApp from './components/app.vue';
import incidentTabs from './components/incidents/incident_tabs.vue';
-import { issueState } from './constants';
+import { issueState, IncidentType } from './constants';
import apolloProvider from './graphql';
import getIssueStateQuery from './queries/get_issue_state.query.graphql';
+import HeaderActions from './components/header_actions.vue';
-export default function initIssuableApp(issuableData = {}) {
+const bootstrapApollo = (state = {}) => {
+ return apolloProvider.clients.defaultClient.cache.writeQuery({
+ query: getIssueStateQuery,
+ data: {
+ issueState: state,
+ },
+ });
+};
+
+export function initIncidentApp(issuableData = {}) {
const el = document.getElementById('js-issuable-app');
if (!el) {
return undefined;
}
- apolloProvider.clients.defaultClient.cache.writeQuery({
- query: getIssueStateQuery,
- data: {
- issueState: { ...issueState, issueType: el.dataset.issueType },
- },
- });
+ bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
const {
+ canCreateIncident,
canUpdate,
iid,
projectNamespace,
@@ -39,6 +45,8 @@ export default function initIssuableApp(issuableData = {}) {
issuableApp,
},
provide: {
+ issueType: IncidentType,
+ canCreateIncident,
canUpdate,
fullPath,
iid,
@@ -57,3 +65,35 @@ export default function initIssuableApp(issuableData = {}) {
},
});
}
+
+export function initIncidentHeaderActions(store) {
+ const el = document.querySelector('.js-issue-header-actions');
+
+ if (!el) {
+ return undefined;
+ }
+
+ bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ store,
+ provide: {
+ canCreateIssue: parseBoolean(el.dataset.canCreateIncident),
+ canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
+ canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
+ canReportSpam: parseBoolean(el.dataset.canReportSpam),
+ canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
+ iid: el.dataset.iid,
+ isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
+ issueType: el.dataset.issueType,
+ newIssuePath: el.dataset.newIssuePath,
+ projectPath: el.dataset.projectPath,
+ projectId: el.dataset.projectId,
+ reportAbusePath: el.dataset.reportAbusePath,
+ submitAsSpamPath: el.dataset.submitAsSpamPath,
+ },
+ render: (createElement) => createElement(HeaderActions),
+ });
+}
diff --git a/app/assets/javascripts/issue_show/issue.js b/app/assets/javascripts/issue_show/issue.js
index 4374dba6eb7..25cc51478ff 100644
--- a/app/assets/javascripts/issue_show/issue.js
+++ b/app/assets/javascripts/issue_show/issue.js
@@ -25,17 +25,22 @@ export function initIssuableApp(issuableData, store) {
bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
+ const { canCreateIncident, ...issuableProps } = issuableData;
+
return new Vue({
el,
apolloProvider,
store,
+ provide: {
+ canCreateIncident,
+ },
computed: {
...mapGetters(['getNoteableData']),
},
render(createElement) {
return createElement(IssuableApp, {
props: {
- ...issuableData,
+ ...issuableProps,
isConfidential: this.getNoteableData?.confidential,
isLocked: this.getNoteableData?.discussion_locked,
issuableStatus: this.getNoteableData?.state,
diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue
index 7b51f6ee46a..7f2082e5b90 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -36,6 +36,7 @@ import {
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
UPDATED_DESC,
@@ -65,6 +66,7 @@ import {
TOKEN_TITLE_LABEL,
TOKEN_TITLE_MILESTONE,
TOKEN_TITLE_MY_REACTION,
+ TOKEN_TITLE_RELEASE,
TOKEN_TITLE_TYPE,
TOKEN_TITLE_WEIGHT,
} from '~/vue_shared/components/filtered_search_bar/constants';
@@ -88,6 +90,8 @@ const LabelToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue');
const MilestoneToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
+const ReleaseToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/release_token.vue');
const WeightToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue');
@@ -165,6 +169,9 @@ export default {
newIssuePath: {
default: '',
},
+ releasesPath: {
+ default: '',
+ },
rssPath: {
default: '',
},
@@ -288,6 +295,7 @@ export default {
avatar_url: gon.current_user_avatar_url,
});
}
+
const tokens = [
{
type: TOKEN_TYPE_AUTHOR,
@@ -297,7 +305,6 @@ export default {
dataType: 'user',
unique: true,
defaultAuthors: [],
- operators: OPERATOR_IS_ONLY,
fetchAuthors: this.fetchUsers,
preloadedAuthors,
},
@@ -317,7 +324,6 @@ export default {
title: TOKEN_TITLE_MILESTONE,
icon: 'clock',
token: MilestoneToken,
- unique: true,
fetchMilestones: this.fetchMilestones,
},
{
@@ -333,7 +339,6 @@ export default {
title: TOKEN_TITLE_TYPE,
icon: 'issues',
token: GlFilteredSearchToken,
- operators: OPERATOR_IS_ONLY,
options: [
{ icon: 'issue-type-issue', title: 'issue', value: 'issue' },
{ icon: 'issue-type-incident', title: 'incident', value: 'incident' },
@@ -342,6 +347,16 @@ export default {
},
];
+ if (this.isProject) {
+ tokens.push({
+ type: TOKEN_TYPE_RELEASE,
+ title: TOKEN_TITLE_RELEASE,
+ icon: 'rocket',
+ token: ReleaseToken,
+ fetchReleases: this.fetchReleases,
+ });
+ }
+
if (this.isSignedIn) {
tokens.push({
type: TOKEN_TYPE_MY_REACTION,
@@ -349,7 +364,6 @@ export default {
icon: 'thumb-up',
token: EmojiToken,
unique: true,
- operators: OPERATOR_IS_ONLY,
fetchEmojis: this.fetchEmojis,
});
@@ -373,7 +387,6 @@ export default {
title: TOKEN_TITLE_ITERATION,
icon: 'iteration',
token: IterationToken,
- unique: true,
fetchIterations: this.fetchIterations,
});
}
@@ -459,6 +472,9 @@ export default {
fetchEmojis(search) {
return this.fetchWithCache(this.autocompleteAwardEmojisPath, 'emojis', 'name', search);
},
+ fetchReleases(search) {
+ return this.fetchWithCache(this.releasesPath, 'releases', 'tag', search);
+ },
fetchLabels(search) {
return this.$apollo
.query({
diff --git a/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue b/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue
index 037fd9be542..e749579af80 100644
--- a/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue
+++ b/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue
@@ -71,8 +71,11 @@ export default {
hasSelectedProject() {
return this.selectedProject.id;
},
+ projectsWithIssuesEnabled() {
+ return this.projects.filter((project) => project.issuesEnabled);
+ },
showNoSearchResultsText() {
- return !this.projects.length && this.search;
+ return !this.projectsWithIssuesEnabled.length && this.search;
},
},
methods: {
@@ -110,7 +113,7 @@ export default {
<gl-loading-icon v-if="$apollo.queries.projects.loading" />
<template v-else>
<gl-dropdown-item
- v-for="project of projects"
+ v-for="project of projectsWithIssuesEnabled"
:key="project.id"
@click="selectProject(project)"
>
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index 5bdc1bd9f90..da9b96d0e22 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
@@ -166,6 +166,7 @@ const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc';
const POPULARITY_ASC_SORT = 'popularity_asc';
const WEIGHT_DESC_SORT = 'weight_desc';
const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc';
+const TITLE_ASC_SORT = 'title_asc';
const TITLE_DESC_SORT = 'title_desc';
export const urlSortParams = {
@@ -187,7 +188,7 @@ export const urlSortParams = {
[WEIGHT_ASC]: WEIGHT,
[WEIGHT_DESC]: WEIGHT_DESC_SORT,
[BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_SORT,
- [TITLE_ASC]: TITLE,
+ [TITLE_ASC]: TITLE_ASC_SORT,
[TITLE_DESC]: TITLE_DESC_SORT,
};
@@ -211,6 +212,7 @@ export const TOKEN_TYPE_ASSIGNEE = 'assignee_username';
export const TOKEN_TYPE_MILESTONE = 'milestone';
export const TOKEN_TYPE_LABEL = 'labels';
export const TOKEN_TYPE_TYPE = 'type';
+export const TOKEN_TYPE_RELEASE = 'release';
export const TOKEN_TYPE_MY_REACTION = 'my_reaction_emoji';
export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
export const TOKEN_TYPE_ITERATION = 'iteration';
@@ -271,6 +273,7 @@ export const filters = {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'label_name[]',
[SPECIAL_FILTER]: 'label_name[]',
+ [ALTERNATIVE_FILTER]: 'label_name',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]',
@@ -280,12 +283,28 @@ export const filters = {
[TOKEN_TYPE_TYPE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'types',
- [SPECIAL_FILTER]: 'types',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'type[]',
- [SPECIAL_FILTER]: 'type[]',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[type][]',
+ },
+ },
+ },
+ [TOKEN_TYPE_RELEASE]: {
+ [API_PARAM]: {
+ [NORMAL_FILTER]: 'releaseTag',
+ [SPECIAL_FILTER]: 'releaseTagWildcardId',
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'release_tag',
+ [SPECIAL_FILTER]: 'release_tag',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[release_tag]',
},
},
},
@@ -299,6 +318,9 @@ export const filters = {
[NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji',
},
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[my_reaction_emoji]',
+ },
},
},
[TOKEN_TYPE_CONFIDENTIAL]: {
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js
index 47af20f5271..59034964afb 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -24,7 +24,7 @@ export function mountJiraIssuesListApp() {
}
Vue.use(VueApollo);
- const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
+ const defaultClient = createDefaultClient();
const apolloProvider = new VueApollo({
defaultClient,
});
@@ -103,7 +103,7 @@ export function mountIssuesListApp() {
},
};
- const defaultClient = createDefaultClient(resolvers, { assumeImmutableResults: true });
+ const defaultClient = createDefaultClient(resolvers);
const apolloProvider = new VueApollo({
defaultClient,
});
@@ -137,6 +137,7 @@ export function mountIssuesListApp() {
newIssuePath,
projectImportJiraPath,
quickActionsHelpPath,
+ releasesPath,
resetPath,
rssPath,
showNewIssueLink,
@@ -164,6 +165,7 @@ export function mountIssuesListApp() {
isSignedIn: parseBoolean(isSignedIn),
jiraIntegrationPath,
newIssuePath,
+ releasesPath,
rssPath,
showNewIssueLink: parseBoolean(showNewIssueLink),
signInPath,
diff --git a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
index 6df72cf6596..9866efbcecc 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
@@ -11,9 +11,13 @@ query getIssues(
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
+ $confidential: Boolean
$labelName: [String]
$milestoneTitle: [String]
$milestoneWildcardId: MilestoneWildcardId
+ $myReactionEmoji: String
+ $releaseTag: [String!]
+ $releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
$not: NegatedIssueFilterInput
$beforeCursor: String
@@ -30,9 +34,11 @@ query getIssues(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
types: $types
not: $not
before: $beforeCursor
@@ -57,9 +63,13 @@ query getIssues(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
before: $beforeCursor
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
index 7bcdbbb28fc..5e755ec5870 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
@@ -5,9 +5,13 @@ query getIssuesCount(
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
+ $confidential: Boolean
$labelName: [String]
$milestoneTitle: [String]
$milestoneWildcardId: MilestoneWildcardId
+ $myReactionEmoji: String
+ $releaseTag: [String!]
+ $releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
$not: NegatedIssueFilterInput
) {
@@ -19,9 +23,11 @@ query getIssuesCount(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
types: $types
not: $not
) {
@@ -34,9 +40,11 @@ query getIssuesCount(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
types: $types
not: $not
) {
@@ -49,9 +57,11 @@ query getIssuesCount(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
types: $types
not: $not
) {
@@ -65,9 +75,13 @@ query getIssuesCount(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
) {
@@ -79,9 +93,13 @@ query getIssuesCount(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
) {
@@ -93,9 +111,13 @@ query getIssuesCount(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
) {
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
index 8f9b888d19b..8c95e6114d3 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
@@ -1,4 +1,4 @@
-query($fullPath: ID!) {
+query getIssuesListDetails($fullPath: ID!) {
project(fullPath: $fullPath) {
issues {
nodes {
diff --git a/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql b/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql
index 78a368089a8..4f7217be7f7 100644
--- a/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql
+++ b/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql
@@ -1,4 +1,10 @@
fragment Iteration on Iteration {
id
title
+ startDate
+ dueDate
+ iterationCadence {
+ id
+ title
+ }
}
diff --git a/app/assets/javascripts/issues_list/queries/search_projects.query.graphql b/app/assets/javascripts/issues_list/queries/search_projects.query.graphql
index df1f330139a..75463f643a2 100644
--- a/app/assets/javascripts/issues_list/queries/search_projects.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/search_projects.query.graphql
@@ -3,6 +3,7 @@ query searchProjects($fullPath: ID!, $search: String) {
projects(search: $search, includeSubgroups: true) {
nodes {
id
+ issuesEnabled
name
nameWithNamespace
webUrl
diff --git a/app/assets/javascripts/issues_list/service_desk_helper.js b/app/assets/javascripts/issues_list/service_desk_helper.js
index f96567ef53b..815f338f1a0 100644
--- a/app/assets/javascripts/issues_list/service_desk_helper.js
+++ b/app/assets/javascripts/issues_list/service_desk_helper.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
/**
* Generates empty state messages for Service Desk issues list.
@@ -20,12 +20,12 @@ export function generateMessages(emptyStateMeta) {
);
const serviceDeskSupportedMessage = s__(
- 'ServiceDesk|Issues created from Service Desk emails appear here. Each comment becomes part of the email conversation.',
+ 'ServiceDesk|Issues created from Service Desk emails will appear here. Each comment becomes part of the email conversation.',
);
const commonDescription = `
<span>${serviceDeskSupportedMessage}</span>
- <a href="${serviceDeskHelpPage}">${s__('Learn more.')}</a>`;
+ <a href="${serviceDeskHelpPage}">${__('Learn more.')}</a>`;
return {
serviceDeskEnabledAndCanEditProjectSettings: {
@@ -60,7 +60,7 @@ export function generateMessages(emptyStateMeta) {
'ServiceDesk|To enable Service Desk on this instance, an instance administrator must first set up incoming email.',
),
primaryLink: incomingEmailHelpPage,
- primaryText: s__('Learn more.'),
+ primaryText: __('Learn more.'),
},
serviceDeskIsNotEnabled: {
title: s__('ServiceDesk|Service Desk is not enabled'),
diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues_list/utils.js
index 1d3d07475af..0e57e2bff83 100644
--- a/app/assets/javascripts/issues_list/utils.js
+++ b/app/assets/javascripts/issues_list/utils.js
@@ -21,9 +21,13 @@ import {
RELATIVE_POSITION_ASC,
SPECIAL_FILTER,
SPECIAL_FILTER_VALUES,
+ TITLE_ASC,
+ TITLE_DESC,
TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_ITERATION,
TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
UPDATED_ASC,
UPDATED_DESC,
@@ -113,11 +117,19 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
descending: RELATIVE_POSITION_ASC,
},
},
+ {
+ id: 9,
+ title: __('Title'),
+ sortDirection: {
+ ascending: TITLE_ASC,
+ descending: TITLE_DESC,
+ },
+ },
];
if (hasIssueWeightsFeature) {
sortOptions.push({
- id: 9,
+ id: sortOptions.length + 1,
title: __('Weight'),
sortDirection: {
ascending: WEIGHT_ASC,
@@ -128,7 +140,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
if (hasBlockedIssuesFeature) {
sortOptions.push({
- id: 10,
+ id: sortOptions.length + 1,
title: __('Blocking'),
sortDirection: {
ascending: BLOCKING_ISSUES_DESC,
@@ -193,17 +205,23 @@ const getFilterType = (data, tokenType = '') =>
? SPECIAL_FILTER
: NORMAL_FILTER;
+const wildcardTokens = [TOKEN_TYPE_ITERATION, TOKEN_TYPE_MILESTONE, TOKEN_TYPE_RELEASE];
+
const isWildcardValue = (tokenType, value) =>
- (tokenType === TOKEN_TYPE_ITERATION || tokenType === TOKEN_TYPE_MILESTONE) &&
- SPECIAL_FILTER_VALUES.includes(value);
+ wildcardTokens.includes(tokenType) && SPECIAL_FILTER_VALUES.includes(value);
const requiresUpperCaseValue = (tokenType, value) =>
tokenType === TOKEN_TYPE_TYPE || isWildcardValue(tokenType, value);
-const formatData = (token) =>
- requiresUpperCaseValue(token.type, token.value.data)
- ? token.value.data.toUpperCase()
- : token.value.data;
+const formatData = (token) => {
+ if (requiresUpperCaseValue(token.type, token.value.data)) {
+ return token.value.data.toUpperCase();
+ }
+ if (token.type === TOKEN_TYPE_CONFIDENTIAL) {
+ return token.value.data === 'yes';
+ }
+ return token.value.data;
+};
export const convertToApiParams = (filterTokens) => {
const params = {};
diff --git a/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql b/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql
index e768154e210..32fbc1113bc 100644
--- a/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql
+++ b/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql
@@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
-query getProjects(
+query jiraGetProjects(
$search: String!
$after: String = ""
$first: Int!
diff --git a/app/assets/javascripts/jira_connect/branches/index.js b/app/assets/javascripts/jira_connect/branches/index.js
index 95bd4f5c675..04510fcff4b 100644
--- a/app/assets/javascripts/jira_connect/branches/index.js
+++ b/app/assets/javascripts/jira_connect/branches/index.js
@@ -14,12 +14,7 @@ export default async function initJiraConnectBranches() {
const { initialBranchName, successStateSvgPath } = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_button.vue
new file mode 100644
index 00000000000..0b286bc903f
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_button.vue
@@ -0,0 +1,24 @@
+<script>
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { ADD_NAMESPACE_MODAL_ID } from '../constants';
+import AddNamespaceModal from './add_namespace_modal/add_namespace_modal.vue';
+
+export default {
+ components: {
+ GlButton,
+ AddNamespaceModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ ADD_NAMESPACE_MODAL_ID,
+};
+</script>
+<template>
+ <div>
+ <gl-button v-gl-modal="$options.ADD_NAMESPACE_MODAL_ID" category="primary" variant="info">
+ {{ s__('Integrations|Add namespace') }}
+ </gl-button>
+ <add-namespace-modal />
+ </div>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal.vue
new file mode 100644
index 00000000000..0e209a09b16
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import { ADD_NAMESPACE_MODAL_ID } from '../../constants';
+import GroupsList from './groups_list.vue';
+
+export default {
+ components: { GlModal, GroupsList },
+ modal: {
+ id: ADD_NAMESPACE_MODAL_ID,
+ title: s__('Integrations|Link namespaces'),
+ cancelProps: {
+ text: __('Cancel'),
+ },
+ },
+};
+</script>
+<template>
+ <gl-modal
+ :modal-id="$options.modal.id"
+ :title="$options.modal.title"
+ :action-cancel="$options.modal.cancelProps"
+ >
+ <groups-list />
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/groups_list.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue
index 5a49d7c1a90..5a49d7c1a90 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/groups_list.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue
new file mode 100644
index 00000000000..005c3bcd0e3
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue
@@ -0,0 +1,85 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { addSubscription } from '~/jira_connect/subscriptions/api';
+import { persistAlert, reloadPage } from '~/jira_connect/subscriptions/utils';
+import { s__ } from '~/locale';
+import GroupItemName from '../group_item_name.vue';
+
+export default {
+ components: {
+ GlButton,
+ GroupItemName,
+ },
+ inject: {
+ subscriptionsPath: {
+ default: '',
+ },
+ },
+ props: {
+ group: {
+ type: Object,
+ required: true,
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ onClick() {
+ this.isLoading = true;
+
+ addSubscription(this.subscriptionsPath, this.group.full_path)
+ .then(() => {
+ persistAlert({
+ title: s__('Integrations|Namespace successfully linked'),
+ message: s__(
+ 'Integrations|You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}',
+ ),
+ linkUrl: helpPagePath('integration/jira_development_panel.html', { anchor: 'usage' }),
+ variant: 'success',
+ });
+
+ reloadPage();
+ })
+ .catch((error) => {
+ this.$emit(
+ 'error',
+ error?.response?.data?.error ||
+ s__('Integrations|Failed to link namespace. Please try again.'),
+ );
+ this.isLoading = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
+ <div class="gl-display-flex gl-align-items-center gl-py-3">
+ <div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center">
+ <div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1">
+ <group-item-name :group="group" />
+ </div>
+
+ <gl-button
+ category="secondary"
+ variant="confirm"
+ :loading="isLoading"
+ :disabled="disabled"
+ @click.prevent="onClick"
+ >
+ {{ __('Link') }}
+ </gl-button>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
index 413424be28d..c0504cbb645 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
@@ -1,65 +1,51 @@
<script>
-import { GlAlert, GlButton, GlLink, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlLink, GlSprintf, GlEmptyState } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
import { mapState, mapMutations } from 'vuex';
-import { retrieveAlert, getLocation } from '~/jira_connect/subscriptions/utils';
-import { __ } from '~/locale';
+import { retrieveAlert } from '~/jira_connect/subscriptions/utils';
import { SET_ALERT } from '../store/mutation_types';
-import GroupsList from './groups_list.vue';
import SubscriptionsList from './subscriptions_list.vue';
+import AddNamespaceButton from './add_namespace_button.vue';
+import SignInButton from './sign_in_button.vue';
export default {
name: 'JiraConnectApp',
components: {
GlAlert,
- GlButton,
GlLink,
- GlModal,
GlSprintf,
- GroupsList,
+ GlEmptyState,
SubscriptionsList,
- },
- directives: {
- GlModalDirective,
+ AddNamespaceButton,
+ SignInButton,
},
inject: {
usersPath: {
default: '',
},
- },
- data() {
- return {
- location: '',
- };
+ subscriptions: {
+ default: [],
+ },
},
computed: {
...mapState(['alert']),
- usersPathWithReturnTo() {
- if (this.location) {
- return `${this.usersPath}?return_to=${this.location}`;
- }
-
- return this.usersPath;
- },
shouldShowAlert() {
return Boolean(this.alert?.message);
},
- },
- modal: {
- cancelProps: {
- text: __('Cancel'),
+ hasSubscriptions() {
+ return !isEmpty(this.subscriptions);
+ },
+ userSignedIn() {
+ return Boolean(!this.usersPath);
},
},
created() {
this.setInitialAlert();
- this.setLocation();
},
methods: {
...mapMutations({
setAlert: SET_ALERT,
}),
- async setLocation() {
- this.location = await getLocation();
- },
setInitialAlert() {
const { linkUrl, title, message, variant } = retrieveAlert() || {};
this.setAlert({ linkUrl, title, message, variant });
@@ -88,38 +74,44 @@ export default {
</template>
</gl-alert>
- <h2 class="gl-text-center">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
+ <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
+ <div class="jira-connect-app-body gl-mx-auto gl-px-5 gl-mb-7">
+ <template v-if="hasSubscriptions">
+ <div class="gl-display-flex gl-justify-content-end">
+ <sign-in-button v-if="!userSignedIn" :users-path="usersPath" />
+ <add-namespace-button v-else />
+ </div>
- <div class="jira-connect-app-body gl-my-7 gl-px-5 gl-pb-4">
- <div class="gl-display-flex gl-justify-content-end">
- <gl-button
- v-if="usersPath"
- category="primary"
- variant="info"
- class="gl-align-self-center"
- :href="usersPathWithReturnTo"
- target="_blank"
- >{{ s__('Integrations|Sign in to add namespaces') }}</gl-button
+ <subscriptions-list />
+ </template>
+ <template v-else>
+ <div v-if="!userSignedIn" class="gl-text-center">
+ <p class="gl-mb-7">{{ s__('JiraService|Sign in to GitLab.com to get started.') }}</p>
+ <sign-in-button class="gl-mb-7" :users-path="usersPath">
+ {{ __('Sign in to GitLab') }}
+ </sign-in-button>
+ <p>
+ {{
+ s__(
+ 'Integrations|Note: this integration only works with accounts on GitLab.com (SaaS).',
+ )
+ }}
+ </p>
+ </div>
+ <gl-empty-state
+ v-else
+ :title="s__('Integrations|No linked namespaces')"
+ :description="
+ s__(
+ 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
+ )
+ "
>
- <template v-else>
- <gl-button
- v-gl-modal-directive="'add-namespace-modal'"
- category="primary"
- variant="info"
- class="gl-align-self-center"
- >{{ s__('Integrations|Add namespace') }}</gl-button
- >
- <gl-modal
- modal-id="add-namespace-modal"
- :title="s__('Integrations|Link namespaces')"
- :action-cancel="$options.modal.cancelProps"
- >
- <groups-list />
- </gl-modal>
- </template>
- </div>
-
- <subscriptions-list />
+ <template #actions>
+ <add-namespace-button />
+ </template>
+ </gl-empty-state>
+ </template>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/groups_list_item.vue b/app/assets/javascripts/jira_connect/subscriptions/components/groups_list_item.vue
deleted file mode 100644
index ed7585e8a88..00000000000
--- a/app/assets/javascripts/jira_connect/subscriptions/components/groups_list_item.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { addSubscription } from '~/jira_connect/subscriptions/api';
-import { persistAlert, reloadPage } from '~/jira_connect/subscriptions/utils';
-import { s__ } from '~/locale';
-import GroupItemName from './group_item_name.vue';
-
-export default {
- components: {
- GlButton,
- GroupItemName,
- },
- inject: {
- subscriptionsPath: {
- default: '',
- },
- },
- props: {
- group: {
- type: Object,
- required: true,
- },
- disabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- isLoading: false,
- };
- },
- methods: {
- onClick() {
- this.isLoading = true;
-
- addSubscription(this.subscriptionsPath, this.group.full_path)
- .then(() => {
- persistAlert({
- title: s__('Integrations|Namespace successfully linked'),
- message: s__(
- 'Integrations|You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}',
- ),
- linkUrl: helpPagePath('integration/jira_development_panel.html', { anchor: 'usage' }),
- variant: 'success',
- });
-
- reloadPage();
- })
- .catch((error) => {
- this.$emit(
- 'error',
- error?.response?.data?.error ||
- s__('Integrations|Failed to link namespace. Please try again.'),
- );
- this.isLoading = false;
- });
- },
- },
-};
-</script>
-
-<template>
- <li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
- <div class="gl-display-flex gl-align-items-center gl-py-3">
- <div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center">
- <div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1">
- <group-item-name :group="group" />
- </div>
-
- <gl-button
- category="secondary"
- variant="confirm"
- :loading="isLoading"
- :disabled="disabled"
- @click.prevent="onClick"
- >
- {{ __('Link') }}
- </gl-button>
- </div>
- </div>
- </li>
-</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue
new file mode 100644
index 00000000000..dc0a77e99c2
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue
@@ -0,0 +1,36 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ usersPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ signInURL: '',
+ };
+ },
+ created() {
+ this.setSignInURL();
+ },
+ methods: {
+ async setSignInURL() {
+ this.signInURL = await getGitlabSignInURL(this.usersPath);
+ },
+ },
+};
+</script>
+<template>
+ <gl-button category="primary" variant="info" :href="signInURL" target="_blank">
+ <slot>
+ {{ s__('Integrations|Sign in to add namespaces') }}
+ </slot>
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
index 7062fb370ed..33126040c16 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
+import { GlButton, GlTable } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapMutations } from 'vuex';
import { removeSubscription } from '~/jira_connect/subscriptions/api';
@@ -12,7 +12,6 @@ import GroupItemName from './group_item_name.vue';
export default {
components: {
GlButton,
- GlEmptyState,
GlTable,
GroupItemName,
TimeagoTooltip,
@@ -44,17 +43,15 @@ export default {
},
],
i18n: {
- emptyTitle: s__('Integrations|No linked namespaces'),
- emptyDescription: s__(
- 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
- ),
unlinkError: s__('Integrations|Failed to unlink namespace. Please try again.'),
},
methods: {
...mapMutations({
setAlert: SET_ALERT,
}),
- isEmpty,
+ isUnlinkButtonDisabled(item) {
+ return !isEmpty(item);
+ },
isLoadingItem(item) {
return this.loadingItem === item;
},
@@ -81,29 +78,22 @@ export default {
</script>
<template>
- <div>
- <gl-empty-state
- v-if="isEmpty(subscriptions)"
- :title="$options.i18n.emptyTitle"
- :description="$options.i18n.emptyDescription"
- />
- <gl-table v-else :items="subscriptions" :fields="$options.fields">
- <template #cell(name)="{ item }">
- <group-item-name :group="item.group" />
- </template>
- <template #cell(created_at)="{ item }">
- <timeago-tooltip :time="item.created_at" />
- </template>
- <template #cell(actions)="{ item }">
- <gl-button
- :class="unlinkBtnClass(item)"
- category="secondary"
- :loading="isLoadingItem(item)"
- :disabled="!isEmpty(loadingItem)"
- @click.prevent="onClick(item)"
- >{{ __('Unlink') }}</gl-button
- >
- </template>
- </gl-table>
- </div>
+ <gl-table :items="subscriptions" :fields="$options.fields">
+ <template #cell(name)="{ item }">
+ <group-item-name :group="item.group" />
+ </template>
+ <template #cell(created_at)="{ item }">
+ <timeago-tooltip :time="item.created_at" />
+ </template>
+ <template #cell(actions)="{ item }">
+ <gl-button
+ :class="unlinkBtnClass(item)"
+ category="secondary"
+ :loading="isLoadingItem(item)"
+ :disabled="isUnlinkButtonDisabled(loadingItem)"
+ @click.prevent="onClick(item)"
+ >{{ __('Unlink') }}</gl-button
+ >
+ </template>
+ </gl-table>
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js
index 8dff83eabb5..2a65b7bc1fa 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/constants.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js
@@ -1,3 +1,5 @@
export const DEFAULT_GROUPS_PER_PAGE = 10;
export const ALERT_LOCALSTORAGE_KEY = 'gitlab_alert';
export const MINIMUM_SEARCH_TERM_LENGTH = 3;
+
+export const ADD_NAMESPACE_MODAL_ID = 'add-namespace-modal';
diff --git a/app/assets/javascripts/jira_connect/subscriptions/index.js b/app/assets/javascripts/jira_connect/subscriptions/index.js
index f1262be0174..8a7a80d885d 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/index.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/index.js
@@ -7,16 +7,20 @@ import Translate from '~/vue_shared/translate';
import JiraConnectApp from './components/app.vue';
import createStore from './store';
-import { getLocation, sizeToParent } from './utils';
+import { getGitlabSignInURL, sizeToParent } from './utils';
const store = createStore();
+/**
+ * Add `return_to` query param to all HAML-defined GitLab sign in links.
+ */
const updateSignInLinks = async () => {
- const location = await getLocation();
- Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => {
- const updatedLink = `${el.getAttribute('href')}?return_to=${location}`;
- el.setAttribute('href', updatedLink);
- });
+ await Promise.all(
+ Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).map(async (el) => {
+ const updatedLink = await getGitlabSignInURL(el.getAttribute('href'));
+ el.setAttribute('href', updatedLink);
+ }),
+ );
};
export async function initJiraConnect() {
diff --git a/app/assets/javascripts/jira_connect/subscriptions/utils.js b/app/assets/javascripts/jira_connect/subscriptions/utils.js
index ed7a9484a81..b2d03a1fbba 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/utils.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/utils.js
@@ -1,4 +1,5 @@
import AccessorUtilities from '~/lib/utils/accessor';
+import { objectToQuery } from '~/lib/utils/url_utility';
import { ALERT_LOCALSTORAGE_KEY } from './constants';
const isFunction = (fn) => typeof fn === 'function';
@@ -71,3 +72,17 @@ export const sizeToParent = () => {
AP.sizeToParent();
}
};
+
+export const getGitlabSignInURL = async (signInURL) => {
+ const location = await getLocation();
+
+ if (location) {
+ const queryParams = {
+ return_to: location,
+ };
+
+ return `${signInURL}?${objectToQuery(queryParams)}`;
+ }
+
+ return signInURL;
+};
diff --git a/app/assets/javascripts/jira_import/index.js b/app/assets/javascripts/jira_import/index.js
index 003f3c7107e..695a237bf50 100644
--- a/app/assets/javascripts/jira_import/index.js
+++ b/app/assets/javascripts/jira_import/index.js
@@ -6,7 +6,7 @@ import App from './components/jira_import_app.vue';
Vue.use(VueApollo);
-const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
+const defaultClient = createDefaultClient();
const apolloProvider = new VueApollo({
defaultClient,
diff --git a/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql b/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql
index 2aacc5cf668..6fec07cc6f8 100644
--- a/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql
+++ b/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql
@@ -1,6 +1,6 @@
#import "./jira_import.fragment.graphql"
-query($fullPath: ID!) {
+query getJiraImportDetails($fullPath: ID!) {
project(fullPath: $fullPath) {
jiraImportStatus
jiraImports {
diff --git a/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql b/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql
index cca33af342c..7af30ffb869 100644
--- a/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql
+++ b/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql
@@ -1,4 +1,4 @@
-mutation($input: JiraImportUsersInput!) {
+mutation getJiraUserMapping($input: JiraImportUsersInput!) {
jiraImportUsers(input: $input) {
jiraUsers {
jiraAccountId
diff --git a/app/assets/javascripts/jira_import/queries/initiate_jira_import.mutation.graphql b/app/assets/javascripts/jira_import/queries/initiate_jira_import.mutation.graphql
index 807374bf06c..29f8428fbcf 100644
--- a/app/assets/javascripts/jira_import/queries/initiate_jira_import.mutation.graphql
+++ b/app/assets/javascripts/jira_import/queries/initiate_jira_import.mutation.graphql
@@ -1,6 +1,6 @@
#import "./jira_import.fragment.graphql"
-mutation($input: JiraImportStartInput!) {
+mutation initiateJiraImport($input: JiraImportStartInput!) {
jiraImportStart(input: $input) {
jiraImport {
...JiraImport
diff --git a/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql b/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql
index 06f119e75ed..6ea8963e6a6 100644
--- a/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql
+++ b/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql
@@ -1,4 +1,4 @@
-query searchProjectMembers($fullPath: ID!, $search: String) {
+query jiraSearchProjectMembers($fullPath: ID!, $search: String) {
project(fullPath: $fullPath) {
projectMembers(search: $search) {
nodes {
diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue
index 269551ff9aa..7a52a1b0d6b 100644
--- a/app/assets/javascripts/jobs/components/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue
@@ -1,5 +1,12 @@
<script>
-import { GlButton, GlLink, GlSprintf } from '@gitlab/ui';
+import {
+ GlFormInputGroup,
+ GlInputGroupText,
+ GlFormInput,
+ GlButton,
+ GlLink,
+ GlSprintf,
+} from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { mapActions } from 'vuex';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -8,6 +15,9 @@ import { s__ } from '~/locale';
export default {
name: 'ManualVariablesForm',
components: {
+ GlFormInputGroup,
+ GlInputGroupText,
+ GlFormInput,
GlButton,
GlLink,
GlSprintf,
@@ -32,6 +42,9 @@ export default {
value: 'value',
},
i18n: {
+ header: s__('CiVariables|Variables'),
+ keyLabel: s__('CiVariables|Key'),
+ valueLabel: s__('CiVariables|Value'),
keyPlaceholder: s__('CiVariables|Input variable key'),
valuePlaceholder: s__('CiVariables|Input variable value'),
formHelpText: s__(
@@ -40,9 +53,13 @@ export default {
},
data() {
return {
- variables: [],
- key: '',
- secretValue: '',
+ variables: [
+ {
+ key: '',
+ secretValue: '',
+ id: uniqueId(),
+ },
+ ],
triggerBtnDisabled: false,
};
},
@@ -50,40 +67,32 @@ export default {
variableSettings() {
return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
},
- },
- watch: {
- key(newVal) {
- this.handleValueChange(newVal, this.$options.inputTypes.key);
- },
- secretValue(newVal) {
- this.handleValueChange(newVal, this.$options.inputTypes.value);
+ preparedVariables() {
+ // we need to ensure no empty variables are passed to the API
+ // and secretValue should be snake_case when passed to the API
+ return this.variables
+ .filter((variable) => variable.key !== '')
+ .map(({ key, secretValue }) => ({ key, secret_value: secretValue }));
},
},
methods: {
...mapActions(['triggerManualJob']),
- handleValueChange(newValue, type) {
- if (newValue !== '') {
- this.createNewVariable(type);
- this.resetForm();
- }
+ canRemove(index) {
+ return index < this.variables.length - 1;
},
- createNewVariable(type) {
- const newVariable = {
- key: this.key,
- secret_value: this.secretValue,
- id: uniqueId(),
- };
+ addEmptyVariable() {
+ const lastVar = this.variables[this.variables.length - 1];
- this.variables.push(newVariable);
+ if (lastVar.key === '') {
+ return;
+ }
- return this.$nextTick().then(() => {
- this.$refs[`${this.$options.inputTypes[type]}-${newVariable.id}`][0].focus();
+ this.variables.push({
+ key: '',
+ secret_value: '',
+ id: uniqueId(),
});
},
- resetForm() {
- this.key = '';
- this.secretValue = '';
- },
deleteVariable(id) {
this.variables.splice(
this.variables.findIndex((el) => el.id === id),
@@ -93,112 +102,92 @@ export default {
trigger() {
this.triggerBtnDisabled = true;
- this.triggerManualJob(this.variables);
+ this.triggerManualJob(this.preparedVariables);
},
},
};
</script>
<template>
- <div class="col-12" data-testid="manual-vars-form">
- <label>{{ s__('CiVariables|Variables') }}</label>
-
- <div class="ci-table">
- <div class="gl-responsive-table-row table-row-header pb-0 pt-0 border-0" role="row">
- <div class="table-section section-50" role="rowheader">{{ s__('CiVariables|Key') }}</div>
- <div class="table-section section-50" role="rowheader">{{ s__('CiVariables|Value') }}</div>
- </div>
+ <div class="row gl-justify-content-center">
+ <div class="col-10" data-testid="manual-vars-form">
+ <label>{{ $options.i18n.header }}</label>
<div
- v-for="variable in variables"
+ v-for="(variable, index) in variables"
:key="variable.id"
- class="gl-responsive-table-row"
+ class="gl-display-flex gl-align-items-center gl-mb-4"
data-testid="ci-variable-row"
>
- <div class="table-section section-50">
- <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Key') }}</div>
- <div class="table-mobile-content gl-mr-3">
- <input
- :ref="`${$options.inputTypes.key}-${variable.id}`"
- v-model="variable.key"
- :placeholder="$options.i18n.keyPlaceholder"
- class="ci-variable-body-item form-control"
- data-testid="ci-variable-key"
- />
- </div>
- </div>
+ <gl-form-input-group class="gl-mr-4 gl-flex-grow-1">
+ <template #prepend>
+ <gl-input-group-text>
+ {{ $options.i18n.keyLabel }}
+ </gl-input-group-text>
+ </template>
+ <gl-form-input
+ :ref="`${$options.inputTypes.key}-${variable.id}`"
+ v-model="variable.key"
+ :placeholder="$options.i18n.keyPlaceholder"
+ data-testid="ci-variable-key"
+ @change="addEmptyVariable"
+ />
+ </gl-form-input-group>
- <div class="table-section section-50">
- <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Value') }}</div>
- <div class="table-mobile-content gl-mr-3">
- <input
- :ref="`${$options.inputTypes.value}-${variable.id}`"
- v-model="variable.secret_value"
- :placeholder="$options.i18n.valuePlaceholder"
- class="ci-variable-body-item form-control"
- data-testid="ci-variable-value"
- />
- </div>
- </div>
+ <gl-form-input-group class="gl-flex-grow-2">
+ <template #prepend>
+ <gl-input-group-text>
+ {{ $options.i18n.valueLabel }}
+ </gl-input-group-text>
+ </template>
+ <gl-form-input
+ :ref="`${$options.inputTypes.value}-${variable.id}`"
+ v-model="variable.secretValue"
+ :placeholder="$options.i18n.valuePlaceholder"
+ data-testid="ci-variable-value"
+ />
+ </gl-form-input-group>
- <div class="table-section section-10">
- <div class="table-mobile-header" role="rowheader"></div>
- <div class="table-mobile-content justify-content-end">
- <gl-button
- category="tertiary"
- icon="clear"
- :aria-label="__('Delete variable')"
- data-testid="delete-variable-btn"
- @click="deleteVariable(variable.id)"
- />
- </div>
- </div>
+ <!-- delete variable button placeholder to not break flex layout -->
+ <div
+ v-if="!canRemove(index)"
+ class="gl-w-7 gl-mr-3"
+ data-testid="delete-variable-btn-placeholder"
+ ></div>
+
+ <gl-button
+ v-if="canRemove(index)"
+ class="gl-flex-grow-0 gl-flex-basis-0"
+ category="tertiary"
+ variant="danger"
+ icon="clear"
+ :aria-label="__('Delete variable')"
+ data-testid="delete-variable-btn"
+ @click="deleteVariable(variable.id)"
+ />
</div>
- <div class="gl-responsive-table-row">
- <div class="table-section section-50">
- <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Key') }}</div>
- <div class="table-mobile-content gl-mr-3">
- <input
- ref="inputKey"
- v-model="key"
- class="js-input-key form-control"
- :placeholder="$options.i18n.keyPlaceholder"
- />
- </div>
- </div>
- <div class="table-section section-50">
- <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Value') }}</div>
- <div class="table-mobile-content gl-mr-3">
- <input
- ref="inputSecretValue"
- v-model="secretValue"
- class="ci-variable-body-item form-control"
- :placeholder="$options.i18n.valuePlaceholder"
- />
- </div>
- </div>
+ <div class="gl-text-center gl-mt-5">
+ <gl-sprintf :message="$options.i18n.formHelpText">
+ <template #link="{ content }">
+ <gl-link :href="variableSettings" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ <div class="gl-display-flex gl-justify-content-center gl-mt-5">
+ <gl-button
+ class="gl-mt-5"
+ variant="info"
+ category="primary"
+ :aria-label="__('Trigger manual job')"
+ :disabled="triggerBtnDisabled"
+ data-testid="trigger-manual-job-btn"
+ @click="trigger"
+ >
+ {{ action.button_title }}
+ </gl-button>
</div>
- </div>
- <div class="gl-text-center gl-mt-3">
- <gl-sprintf :message="$options.i18n.formHelpText">
- <template #link="{ content }">
- <gl-link :href="variableSettings" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </div>
- <div class="d-flex justify-content-center">
- <gl-button
- variant="info"
- category="primary"
- :aria-label="__('Trigger manual job')"
- :disabled="triggerBtnDisabled"
- data-testid="trigger-manual-job-btn"
- @click="trigger"
- >
- {{ action.button_title }}
- </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index fef5b37015c..b1ddede8fe8 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -84,9 +84,13 @@ export default {
>
</p>
- <gl-table :items="trigger.variables" :fields="$options.fields" small bordered>
+ <gl-table :items="trigger.variables" :fields="$options.fields" small bordered fixed>
+ <template #cell(key)="{ item }">
+ <span class="gl-overflow-break-word">{{ item.key }}</span>
+ </template>
+
<template #cell(value)="data">
- {{ getDisplayValue(data.value) }}
+ <span class="gl-overflow-break-word">{{ getDisplayValue(data.value) }}</span>
</template>
</gl-table>
</template>
diff --git a/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js b/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js
index ad92bd4de42..9b7901685b6 100644
--- a/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js
+++ b/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js
@@ -9,10 +9,6 @@ import { isNavigatingAway } from '~/lib/utils/is_navigating_away';
* @returns {ApolloLink|null}
*/
export const getSuppressNetworkErrorsDuringNavigationLink = () => {
- if (!gon.features?.suppressApolloErrorsDuringNavigation) {
- return null;
- }
-
return onError(({ networkError }) => {
if (networkError && isNavigatingAway()) {
// Return an observable that will never notify any subscribers with any
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 39bf804b54e..df2e85afe24 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -48,7 +48,6 @@ export const stripWhitespaceFromQuery = (url, path) => {
export default (resolvers = {}, config = {}) => {
const {
- assumeImmutableResults,
baseUrl,
batchMax = 10,
cacheConfig,
@@ -161,10 +160,10 @@ export default (resolvers = {}, config = {}) => {
link: appLink,
cache: new InMemoryCache({
...cacheConfig,
- freezeResults: assumeImmutableResults,
+ freezeResults: true,
}),
resolvers,
- assumeImmutableResults,
+ assumeImmutableResults: true,
defaultOptions: {
query: {
fetchPolicy,
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 813fd3dbb1e..a82dad7e2c9 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -220,16 +220,16 @@ export const scrollToElement = (element, options = {}) => {
// In the previous implementation, jQuery naturally deferred this scrolling.
// Unfortunately, we're quite coupled to this implementation detail now.
defer(() => {
- const { duration = 200, offset = 0 } = options;
+ const { duration = 200, offset = 0, behavior = duration ? 'smooth' : 'auto' } = options;
const y = el.getBoundingClientRect().top + window.pageYOffset + offset - contentTop();
- window.scrollTo({ top: y, behavior: duration ? 'smooth' : 'auto' });
+ window.scrollTo({ top: y, behavior });
});
}
};
-export const scrollToElementWithContext = (element) => {
+export const scrollToElementWithContext = (element, options) => {
const offsetMultiplier = -0.1;
- return scrollToElement(element, { offset: window.innerHeight * offsetMultiplier });
+ return scrollToElement(element, { ...options, offset: window.innerHeight * offsetMultiplier });
};
/**
@@ -688,17 +688,20 @@ export const searchBy = (query = '', searchSpace = {}) => {
*/
export const isScopedLabel = ({ title = '' } = {}) => title.includes(SCOPED_LABEL_DELIMITER);
+const scopedLabelRegex = new RegExp(`(.*)${SCOPED_LABEL_DELIMITER}.*`);
+
/**
- * Returns the base value of the scoped label
- *
- * Expected Label to be an Object with `title` as a key:
- * { title: 'LabelTitle', ...otherProperties };
+ * Returns the key of a scoped label.
+ * For example:
+ * - returns `scoped` if the label is `scoped::value`.
+ * - returns `scoped::label` if the label is `scoped::label::value`.
*
- * @param {Object} label
- * @returns String
+ * @param {Object} label object containing `title` property
+ * @returns String scoped label key, or full label if it is not a scoped label
*/
-export const scopedLabelKey = ({ title = '' }) =>
- isScopedLabel({ title }) && title.split(SCOPED_LABEL_DELIMITER)[0];
+export const scopedLabelKey = ({ title = '' }) => {
+ return title.replace(scopedLabelRegex, '$1');
+};
// Methods to set and get Cookie
export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 });
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
new file mode 100644
index 00000000000..733d0f69f5d
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ cancelAction: { text: __('Cancel') },
+ components: {
+ GlModal,
+ },
+ props: {
+ primaryText: {
+ type: String,
+ required: false,
+ default: __('OK'),
+ },
+ primaryVariant: {
+ type: String,
+ required: false,
+ default: 'confirm',
+ },
+ },
+ computed: {
+ primaryAction() {
+ return { text: this.primaryText, attributes: { variant: this.primaryVariant } };
+ },
+ },
+ mounted() {
+ this.$refs.modal.show();
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="modal"
+ size="sm"
+ modal-id="confirmationModal"
+ body-class="gl-display-flex"
+ :action-primary="primaryAction"
+ :action-cancel="$options.cancelAction"
+ hide-header
+ @primary="$emit('confirmed')"
+ @hidden="$emit('closed')"
+ >
+ <div class="gl-align-self-center"><slot></slot></div>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
new file mode 100644
index 00000000000..fdd0e045d07
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+
+export function confirmAction(message, { primaryBtnVariant, primaryBtnText } = {}) {
+ return new Promise((resolve) => {
+ let confirmed = false;
+
+ const component = new Vue({
+ components: {
+ ConfirmModal: () => import('./confirm_modal.vue'),
+ },
+ render(h) {
+ return h(
+ 'confirm-modal',
+ {
+ props: {
+ primaryVariant: primaryBtnVariant,
+ primaryText: primaryBtnText,
+ },
+ on: {
+ confirmed() {
+ confirmed = true;
+ },
+ closed() {
+ component.$destroy();
+ resolve(confirmed);
+ },
+ },
+ },
+ [message],
+ );
+ },
+ }).$mount();
+ });
+}
+
+export function confirmViaGlModal(message, element) {
+ const primaryBtnConfig = {};
+
+ const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant');
+
+ if (confirmBtnVariant) {
+ primaryBtnConfig.primaryBtnVariant = confirmBtnVariant;
+ }
+
+ const screenReaderText =
+ element.querySelector('.gl-sr-only')?.textContent ||
+ element.querySelector('.sr-only')?.textContent ||
+ element.getAttribute('aria-label');
+
+ if (screenReaderText) {
+ primaryBtnConfig.primaryBtnText = screenReaderText;
+ }
+
+ return confirmAction(message, primaryBtnConfig);
+}
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 0e5a23a5cbb..36c6545164e 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -24,3 +24,5 @@ export const DEFAULT_TH_CLASSES =
// We set the drawer's z-index to 252 to clear flash messages that might
// be displayed in the page and that have a z-index of 251.
export const DRAWER_Z_INDEX = 252;
+
+export const MIN_USERNAME_LENGTH = 2;
diff --git a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
index 3c446c21865..7bff2bf3e47 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
@@ -14,33 +14,33 @@ import { s__, n__, __, sprintf } from '../../../locale';
export const getMonthNames = (abbreviated) => {
if (abbreviated) {
return [
- s__('Jan'),
- s__('Feb'),
- s__('Mar'),
- s__('Apr'),
- s__('May'),
- s__('Jun'),
- s__('Jul'),
- s__('Aug'),
- s__('Sep'),
- s__('Oct'),
- s__('Nov'),
- s__('Dec'),
+ __('Jan'),
+ __('Feb'),
+ __('Mar'),
+ __('Apr'),
+ __('May'),
+ __('Jun'),
+ __('Jul'),
+ __('Aug'),
+ __('Sep'),
+ __('Oct'),
+ __('Nov'),
+ __('Dec'),
];
}
return [
- s__('January'),
- s__('February'),
- s__('March'),
- s__('April'),
- s__('May'),
- s__('June'),
- s__('July'),
- s__('August'),
- s__('September'),
- s__('October'),
- s__('November'),
- s__('December'),
+ __('January'),
+ __('February'),
+ __('March'),
+ __('April'),
+ __('May'),
+ __('June'),
+ __('July'),
+ __('August'),
+ __('September'),
+ __('October'),
+ __('November'),
+ __('December'),
];
};
diff --git a/app/assets/javascripts/lib/utils/file_upload.js b/app/assets/javascripts/lib/utils/file_upload.js
index b8b63bf58d4..f99a4927338 100644
--- a/app/assets/javascripts/lib/utils/file_upload.js
+++ b/app/assets/javascripts/lib/utils/file_upload.js
@@ -15,13 +15,17 @@ export default (buttonSelector, fileSelector) => {
});
};
-export const getFilename = ({ clipboardData }) => {
- let value;
- if (window.clipboardData && window.clipboardData.getData) {
- value = window.clipboardData.getData('Text');
- } else if (clipboardData && clipboardData.getData) {
- value = clipboardData.getData('text/plain');
+export const getFilename = (file) => {
+ let fileName;
+ if (file) {
+ fileName = file.name;
}
- value = value.split('\r');
- return value[0];
+
+ return fileName;
+};
+
+export const validateImageName = (file) => {
+ const fileName = file.name ? file.name : 'image.png';
+ const legalImageRegex = /^[\w.\-+]+\.(png|jpg|jpeg|gif|bmp|tiff|ico|webp)$/;
+ return legalImageRegex.test(fileName) ? fileName : 'image.png';
};
diff --git a/app/assets/javascripts/lib/utils/rails_ujs.js b/app/assets/javascripts/lib/utils/rails_ujs.js
index 8b40cc7bd11..6b1985a23ba 100644
--- a/app/assets/javascripts/lib/utils/rails_ujs.js
+++ b/app/assets/javascripts/lib/utils/rails_ujs.js
@@ -1,4 +1,42 @@
import Rails from '@rails/ujs';
+import { confirmViaGlModal } from './confirm_via_gl_modal/confirm_via_gl_modal';
+
+function monkeyPatchConfirmModal() {
+ /**
+ * This function is used to replace the `Rails.confirm` which uses `window.confirm`
+ *
+ * This function opens a confirmation modal which will resolve in a promise.
+ * Because the `Rails.confirm` API is synchronous, we go with a little hack here:
+ *
+ * 1. User clicks on something with `data-confirm`
+ * 2. We open the modal and return `false`, ending the "Rails" event chain
+ * 3. If the modal is closed and the user "confirmed" the action
+ * 1. replace the `Rails.confirm` with a function that always returns `true`
+ * 2. click the same element programmatically
+ *
+ * @param message {String} Message to be shown in the modal
+ * @param element {HTMLElement} Element that was clicked on
+ * @returns {boolean}
+ */
+ function confirmViaModal(message, element) {
+ confirmViaGlModal(message, element)
+ .then((confirmed) => {
+ if (confirmed) {
+ Rails.confirm = () => true;
+ element.click();
+ Rails.confirm = confirmViaModal;
+ }
+ })
+ .catch(() => {});
+ return false;
+ }
+
+ Rails.confirm = confirmViaModal;
+}
+
+if (gon?.features?.bootstrapConfirmationModals) {
+ monkeyPatchConfirmModal();
+}
export const initRails = () => {
// eslint-disable-next-line no-underscore-dangle
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 0804d792631..40dd29bea76 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -233,7 +233,7 @@ export function insertMarkdownText({
}
} else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, () =>
- selected.replace(/\\n/g, '\n').replace('%br', '\\n'),
+ selected.replace(/\\n/g, '\n').replace(/%br/g, '\\n'),
);
} else {
textToInsert = String(startChar) + tag + selected + (wrap ? tag : '');
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index c70d23d06ec..e53a39cde06 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -5,6 +5,12 @@ const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`);
const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`);
const SHA_REGEX = /[\da-f]{40}/gi;
+// About GitLab default host (overwrite in jh)
+export const PROMO_HOST = 'about.gitlab.com';
+
+// About Gitlab default url (overwrite in jh)
+export const PROMO_URL = `https://${PROMO_HOST}`;
+
// Reset the cursor in a Regex so that multiple uses before a recompile don't fail
function resetRegExp(regex) {
regex.lastIndex = 0; /* eslint-disable-line no-param-reassign */
diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue
index 2a60825a427..c9e7b034950 100644
--- a/app/assets/javascripts/logs/components/environment_logs.vue
+++ b/app/assets/javascripts/logs/components/environment_logs.vue
@@ -130,7 +130,7 @@ export default {
}}
<a :href="clusterApplicationsDocumentationPath">
<strong>
- {{ s__('View Documentation') }}
+ {{ __('View Documentation') }}
</strong>
</a>
</gl-alert>
diff --git a/app/assets/javascripts/members/components/app.vue b/app/assets/javascripts/members/components/app.vue
index 0ec39f58930..c5083bc4826 100644
--- a/app/assets/javascripts/members/components/app.vue
+++ b/app/assets/javascripts/members/components/app.vue
@@ -11,7 +11,10 @@ export default {
components: { MembersTable, FilterSortContainer, GlAlert },
provide() {
return {
- namespace: this.namespace,
+ // We can't use this.namespace due to bug in vue-apollo when
+ // provide is called in beforeCreate
+ // See https://github.com/vuejs/vue-apollo/pull/1153 for details
+ namespace: this.$options.propsData.namespace,
};
},
props: {
diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue
deleted file mode 100644
index e5d7e2ea2eb..00000000000
--- a/app/assets/javascripts/monitoring/components/alert_widget.vue
+++ /dev/null
@@ -1,285 +0,0 @@
-<script>
-import { GlBadge, GlLoadingIcon, GlModalDirective, GlIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
-import { values, get } from 'lodash';
-import createFlash from '~/flash';
-import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
-import { s__ } from '~/locale';
-import { OPERATORS } from '../constants';
-import AlertsService from '../services/alerts_service';
-import { alertsValidator, queriesValidator } from '../validators';
-import AlertWidgetForm from './alert_widget_form.vue';
-
-export default {
- components: {
- AlertWidgetForm,
- GlBadge,
- GlLoadingIcon,
- GlIcon,
- GlTooltip,
- GlSprintf,
- },
- directives: {
- GlModal: GlModalDirective,
- },
- props: {
- alertsEndpoint: {
- type: String,
- required: true,
- },
- showLoadingState: {
- type: Boolean,
- required: false,
- default: true,
- },
- // { [alertPath]: { alert_attributes } }. Populated from subsequent API calls.
- // Includes only the metrics/alerts to be managed by this widget.
- alertsToManage: {
- type: Object,
- required: false,
- default: () => ({}),
- validator: alertsValidator,
- },
- // [{ metric+query_attributes }]. Represents queries (and alerts) we know about
- // on intial fetch. Essentially used for reference.
- relevantQueries: {
- type: Array,
- required: true,
- validator: queriesValidator,
- },
- modalId: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- service: null,
- errorMessage: null,
- isLoading: false,
- apiAction: 'create',
- };
- },
- i18n: {
- alertsCountMsg: s__('PrometheusAlerts|%{count} alerts applied'),
- singleFiringMsg: s__('PrometheusAlerts|Firing: %{alert}'),
- multipleFiringMsg: s__('PrometheusAlerts|%{firingCount} firing'),
- firingAlertsTooltip: s__('PrometheusAlerts|Firing: %{alerts}'),
- },
- computed: {
- singleAlertSummary() {
- return {
- message: this.isFiring ? this.$options.i18n.singleFiringMsg : this.thresholds[0],
- alert: this.thresholds[0],
- };
- },
- multipleAlertsSummary() {
- return {
- message: this.isFiring
- ? `${this.$options.i18n.alertsCountMsg}, ${this.$options.i18n.multipleFiringMsg}`
- : this.$options.i18n.alertsCountMsg,
- count: this.thresholds.length,
- firingCount: this.firingAlerts.length,
- };
- },
- shouldShowLoadingIcon() {
- return this.showLoadingState && this.isLoading;
- },
- thresholds() {
- const alertsToManage = Object.keys(this.alertsToManage);
- return alertsToManage.map(this.formatAlertSummary);
- },
- hasAlerts() {
- return Boolean(Object.keys(this.alertsToManage).length);
- },
- hasMultipleAlerts() {
- return this.thresholds.length > 1;
- },
- isFiring() {
- return Boolean(this.firingAlerts.length);
- },
- firingAlerts() {
- return values(this.alertsToManage).filter((alert) =>
- this.passedAlertThreshold(this.getQueryData(alert), alert),
- );
- },
- formattedFiringAlerts() {
- return this.firingAlerts.map((alert) => this.formatAlertSummary(alert.alert_path));
- },
- configuredAlert() {
- return this.hasAlerts ? values(this.alertsToManage)[0].metricId : '';
- },
- },
- created() {
- this.service = new AlertsService({ alertsEndpoint: this.alertsEndpoint });
- this.fetchAlertData();
- },
- methods: {
- fetchAlertData() {
- this.isLoading = true;
-
- const queriesWithAlerts = this.relevantQueries.filter((query) => query.alert_path);
-
- return Promise.all(
- queriesWithAlerts.map((query) =>
- this.service
- .readAlert(query.alert_path)
- .then((alertAttributes) => this.setAlert(alertAttributes, query.metricId)),
- ),
- )
- .then(() => {
- this.isLoading = false;
- })
- .catch(() => {
- createFlash({
- message: s__('PrometheusAlerts|Error fetching alert'),
- });
- this.isLoading = false;
- });
- },
- setAlert(alertAttributes, metricId) {
- this.$emit('setAlerts', alertAttributes.alert_path, { ...alertAttributes, metricId });
- },
- removeAlert(alertPath) {
- this.$emit('setAlerts', alertPath, null);
- },
- formatAlertSummary(alertPath) {
- const alert = this.alertsToManage[alertPath];
- const alertQuery = this.relevantQueries.find((query) => query.metricId === alert.metricId);
-
- return `${alertQuery.label} ${alert.operator} ${alert.threshold}`;
- },
- passedAlertThreshold(data, alert) {
- const { threshold, operator } = alert;
-
- switch (operator) {
- case OPERATORS.greaterThan:
- return data.some((value) => value > threshold);
- case OPERATORS.lessThan:
- return data.some((value) => value < threshold);
- case OPERATORS.equalTo:
- return data.some((value) => value === threshold);
- default:
- return false;
- }
- },
- getQueryData(alert) {
- const alertQuery = this.relevantQueries.find((query) => query.metricId === alert.metricId);
-
- return get(alertQuery, 'result[0].values', []).map((value) => get(value, '[1]', null));
- },
- showModal() {
- this.$root.$emit(BV_SHOW_MODAL, this.modalId);
- },
- hideModal() {
- this.errorMessage = null;
- this.$root.$emit(BV_HIDE_MODAL, this.modalId);
- },
- handleSetApiAction(apiAction) {
- this.apiAction = apiAction;
- },
- handleCreate({ operator, threshold, prometheus_metric_id, runbookUrl }) {
- const newAlert = { operator, threshold, prometheus_metric_id, runbookUrl };
- this.isLoading = true;
- this.service
- .createAlert(newAlert)
- .then((alertAttributes) => {
- this.setAlert(alertAttributes, prometheus_metric_id);
- this.isLoading = false;
- this.hideModal();
- })
- .catch(() => {
- this.errorMessage = s__('PrometheusAlerts|Error creating alert');
- this.isLoading = false;
- });
- },
- handleUpdate({ alert, operator, threshold, runbookUrl }) {
- const updatedAlert = { operator, threshold, runbookUrl };
- this.isLoading = true;
- this.service
- .updateAlert(alert, updatedAlert)
- .then((alertAttributes) => {
- this.setAlert(alertAttributes, this.alertsToManage[alert].metricId);
- this.isLoading = false;
- this.hideModal();
- })
- .catch(() => {
- this.errorMessage = s__('PrometheusAlerts|Error saving alert');
- this.isLoading = false;
- });
- },
- handleDelete({ alert }) {
- this.isLoading = true;
- this.service
- .deleteAlert(alert)
- .then(() => {
- this.removeAlert(alert);
- this.isLoading = false;
- this.hideModal();
- })
- .catch(() => {
- this.errorMessage = s__('PrometheusAlerts|Error deleting alert');
- this.isLoading = false;
- });
- },
- },
-};
-</script>
-
-<template>
- <div class="prometheus-alert-widget dropdown flex-grow-2 overflow-hidden">
- <gl-loading-icon v-if="shouldShowLoadingIcon" :inline="true" size="sm" />
- <span v-else-if="errorMessage" ref="alertErrorMessage" class="alert-error-message">{{
- errorMessage
- }}</span>
- <span
- v-else-if="hasAlerts"
- ref="alertCurrentSetting"
- class="alert-current-setting cursor-pointer d-flex"
- @click="showModal"
- >
- <gl-badge :variant="isFiring ? 'danger' : 'neutral'" class="d-flex-center text-truncate">
- <gl-icon name="warning" :size="16" class="flex-shrink-0" />
- <span class="text-truncate gl-pl-2">
- <gl-sprintf
- :message="
- hasMultipleAlerts ? multipleAlertsSummary.message : singleAlertSummary.message
- "
- >
- <template #alert>
- {{ singleAlertSummary.alert }}
- </template>
- <template #count>
- {{ multipleAlertsSummary.count }}
- </template>
- <template #firingCount>
- {{ multipleAlertsSummary.firingCount }}
- </template>
- </gl-sprintf>
- </span>
- </gl-badge>
- <gl-tooltip v-if="hasMultipleAlerts && isFiring" :target="() => $refs.alertCurrentSetting">
- <gl-sprintf :message="$options.i18n.firingAlertsTooltip">
- <template #alerts>
- <div v-for="alert in formattedFiringAlerts" :key="alert.alert_path">
- {{ alert }}
- </div>
- </template>
- </gl-sprintf>
- </gl-tooltip>
- </span>
- <alert-widget-form
- ref="widgetForm"
- :disabled="isLoading"
- :alerts-to-manage="alertsToManage"
- :relevant-queries="relevantQueries"
- :error-message="errorMessage"
- :configured-alert="configuredAlert"
- :modal-id="modalId"
- @create="handleCreate"
- @update="handleUpdate"
- @delete="handleDelete"
- @cancel="hideModal"
- @setAction="handleSetApiAction"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/alert_widget_form.vue b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
deleted file mode 100644
index 68fd3e256ec..00000000000
--- a/app/assets/javascripts/monitoring/components/alert_widget_form.vue
+++ /dev/null
@@ -1,324 +0,0 @@
-<script>
-import {
- GlLink,
- GlButton,
- GlButtonGroup,
- GlFormGroup,
- GlFormInput,
- GlDropdown,
- GlDropdownItem,
- GlModal,
- GlTooltipDirective,
- GlIcon,
-} from '@gitlab/ui';
-import { isEmpty, findKey } from 'lodash';
-import Vue from 'vue';
-import { __, s__ } from '~/locale';
-import TrackEventDirective from '~/vue_shared/directives/track_event';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import Translate from '~/vue_shared/translate';
-import { OPERATORS } from '../constants';
-import { alertsValidator, queriesValidator } from '../validators';
-
-Vue.use(Translate);
-
-const SUBMIT_ACTION_TEXT = {
- create: __('Add'),
- update: __('Save'),
- delete: __('Delete'),
-};
-
-const SUBMIT_BUTTON_CLASS = {
- create: 'btn-success',
- update: 'btn-success',
- delete: 'btn-danger',
-};
-
-export default {
- components: {
- GlButton,
- GlButtonGroup,
- GlFormGroup,
- GlFormInput,
- GlDropdown,
- GlDropdownItem,
- GlModal,
- GlLink,
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- TrackEvent: TrackEventDirective,
- },
- mixins: [glFeatureFlagsMixin()],
- props: {
- disabled: {
- type: Boolean,
- required: true,
- },
- errorMessage: {
- type: String,
- required: false,
- default: '',
- },
- configuredAlert: {
- type: String,
- required: false,
- default: '',
- },
- alertsToManage: {
- type: Object,
- required: false,
- default: () => ({}),
- validator: alertsValidator,
- },
- relevantQueries: {
- type: Array,
- required: true,
- validator: queriesValidator,
- },
- modalId: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- operators: OPERATORS,
- operator: null,
- threshold: null,
- prometheusMetricId: null,
- runbookUrl: null,
- selectedAlert: {},
- alertQuery: '',
- };
- },
- computed: {
- isValidQuery() {
- // TODO: Add query validation check (most likely via http request)
- return this.alertQuery.length ? true : null;
- },
- currentQuery() {
- return this.relevantQueries.find((query) => query.metricId === this.prometheusMetricId) || {};
- },
- formDisabled() {
- // We need a prometheusMetricId to determine whether we're
- // creating/updating/deleting
- return this.disabled || !(this.prometheusMetricId || this.isValidQuery);
- },
- supportsComputedAlerts() {
- return this.glFeatures.prometheusComputedAlerts;
- },
- queryDropdownLabel() {
- return this.currentQuery.label || s__('PrometheusAlerts|Select query');
- },
- haveValuesChanged() {
- return (
- this.operator &&
- this.threshold === Number(this.threshold) &&
- (this.operator !== this.selectedAlert.operator ||
- this.threshold !== this.selectedAlert.threshold ||
- this.runbookUrl !== this.selectedAlert.runbookUrl)
- );
- },
- submitAction() {
- if (isEmpty(this.selectedAlert)) return 'create';
- if (this.haveValuesChanged) return 'update';
- return 'delete';
- },
- submitActionText() {
- return SUBMIT_ACTION_TEXT[this.submitAction];
- },
- submitButtonClass() {
- return SUBMIT_BUTTON_CLASS[this.submitAction];
- },
- isSubmitDisabled() {
- return this.disabled || (this.submitAction === 'create' && !this.haveValuesChanged);
- },
- dropdownTitle() {
- return this.submitAction === 'create'
- ? s__('PrometheusAlerts|Add alert')
- : s__('PrometheusAlerts|Edit alert');
- },
- },
- watch: {
- alertsToManage() {
- this.resetAlertData();
- },
- submitAction() {
- this.$emit('setAction', this.submitAction);
- },
- },
- methods: {
- selectQuery(queryId) {
- const existingAlertPath = findKey(this.alertsToManage, (alert) => alert.metricId === queryId);
- const existingAlert = this.alertsToManage[existingAlertPath];
-
- if (existingAlert) {
- const { operator, threshold, runbookUrl } = existingAlert;
-
- this.selectedAlert = existingAlert;
- this.operator = operator;
- this.threshold = threshold;
- this.runbookUrl = runbookUrl;
- } else {
- this.selectedAlert = {};
- this.operator = this.operators.greaterThan;
- this.threshold = null;
- this.runbookUrl = null;
- }
-
- this.prometheusMetricId = queryId;
- },
- handleHidden() {
- this.resetAlertData();
- this.$emit('cancel');
- },
- handleSubmit() {
- this.$emit(this.submitAction, {
- alert: this.selectedAlert.alert_path,
- operator: this.operator,
- threshold: this.threshold,
- prometheus_metric_id: this.prometheusMetricId,
- runbookUrl: this.runbookUrl,
- });
- },
- handleShown() {
- if (this.configuredAlert) {
- this.selectQuery(this.configuredAlert);
- } else if (this.relevantQueries.length === 1) {
- this.selectQuery(this.relevantQueries[0].metricId);
- }
- },
- resetAlertData() {
- this.operator = null;
- this.threshold = null;
- this.prometheusMetricId = null;
- this.selectedAlert = {};
- this.runbookUrl = null;
- },
- getAlertFormActionTrackingOption() {
- const label = `${this.submitAction}_alert`;
- return {
- category: document.body.dataset.page,
- action: 'click_button',
- label,
- };
- },
- },
- alertQueryText: {
- label: __('Query'),
- validFeedback: __('Query is valid'),
- invalidFeedback: __('Invalid query'),
- descriptionTooltip: __(
- 'Example: Usage = single query. (Requested) / (Capacity) = multiple queries combined into a formula.',
- ),
- },
-};
-</script>
-
-<template>
- <gl-modal
- ref="alertModal"
- :title="dropdownTitle"
- :modal-id="modalId"
- :ok-variant="submitAction === 'delete' ? 'danger' : 'success'"
- :ok-disabled="formDisabled"
- @ok.prevent="handleSubmit"
- @hidden="handleHidden"
- @shown="handleShown"
- >
- <div v-if="errorMessage" class="alert-modal-message danger_message">{{ errorMessage }}</div>
- <div class="alert-form">
- <gl-form-group
- v-if="supportsComputedAlerts"
- :label="$options.alertQueryText.label"
- label-for="alert-query-input"
- :valid-feedback="$options.alertQueryText.validFeedback"
- :invalid-feedback="$options.alertQueryText.invalidFeedback"
- :state="isValidQuery"
- >
- <gl-form-input id="alert-query-input" v-model.trim="alertQuery" :state="isValidQuery" />
- <template #description>
- <div class="d-flex align-items-center">
- {{ __('Single or combined queries') }}
- <gl-icon
- v-gl-tooltip="$options.alertQueryText.descriptionTooltip"
- name="question"
- class="gl-ml-2"
- />
- </div>
- </template>
- </gl-form-group>
- <gl-form-group v-else label-for="alert-query-dropdown" :label="$options.alertQueryText.label">
- <gl-dropdown
- id="alert-query-dropdown"
- :text="queryDropdownLabel"
- toggle-class="dropdown-menu-toggle gl-border-1! qa-alert-query-dropdown"
- >
- <gl-dropdown-item
- v-for="query in relevantQueries"
- :key="query.metricId"
- data-qa-selector="alert_query_option"
- @click="selectQuery(query.metricId)"
- >
- {{ query.label }}
- </gl-dropdown-item>
- </gl-dropdown>
- </gl-form-group>
- <gl-button-group class="mb-3" :label="s__('PrometheusAlerts|Operator')">
- <gl-button
- :class="{ active: operator === operators.greaterThan }"
- :disabled="formDisabled"
- @click="operator = operators.greaterThan"
- >
- {{ operators.greaterThan }}
- </gl-button>
- <gl-button
- :class="{ active: operator === operators.equalTo }"
- :disabled="formDisabled"
- @click="operator = operators.equalTo"
- >
- {{ operators.equalTo }}
- </gl-button>
- <gl-button
- :class="{ active: operator === operators.lessThan }"
- :disabled="formDisabled"
- @click="operator = operators.lessThan"
- >
- {{ operators.lessThan }}
- </gl-button>
- </gl-button-group>
- <gl-form-group :label="s__('PrometheusAlerts|Threshold')" label-for="alerts-threshold">
- <gl-form-input
- id="alerts-threshold"
- v-model.number="threshold"
- :disabled="formDisabled"
- type="number"
- data-qa-selector="alert_threshold_field"
- />
- </gl-form-group>
- <gl-form-group
- :label="s__('PrometheusAlerts|Runbook URL (optional)')"
- label-for="alert-runbook"
- >
- <gl-form-input
- id="alert-runbook"
- v-model="runbookUrl"
- :disabled="formDisabled"
- data-testid="alertRunbookField"
- type="text"
- :placeholder="s__('PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks')"
- />
- </gl-form-group>
- </div>
- <template #modal-ok>
- <gl-link
- v-track-event="getAlertFormActionTrackingOption()"
- class="text-reset text-decoration-none"
- >
- {{ submitActionText }}
- </gl-link>
- </template>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
index 4b54cffe231..ae079da0b0b 100644
--- a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
+++ b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
@@ -1,8 +1,12 @@
<script>
import chartEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/chart-empty-state.svg';
+import { GlSafeHtmlDirective } from '@gitlab/ui';
import { chartHeight } from '../../constants';
export default {
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
data() {
return {
height: chartHeight,
@@ -18,14 +22,15 @@ export default {
created() {
this.chartEmptyStateIllustration = chartEmptyStateIllustration;
},
+ safeHtmlConfig: { ADD_TAGS: ['use'] },
};
</script>
<template>
<div class="d-flex flex-column justify-content-center">
<div
+ v-safe-html:[$options.safeHtmlConfig]="chartEmptyStateIllustration"
class="gl-mt-3 svg-w-100 d-flex align-items-center"
:style="svgContainerStyle"
- v-html="chartEmptyStateIllustration /* eslint-disable-line vue/no-v-html */"
></div>
<h5 class="text-center gl-mt-3">{{ __('No data to display') }}</h5>
</div>
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 12f5e7efc96..5529a94874b 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -73,11 +73,6 @@ export default {
required: false,
default: chartHeight,
},
- thresholds: {
- type: Array,
- required: false,
- default: () => [],
- },
legendLayout: {
type: String,
required: false,
@@ -391,7 +386,6 @@ export default {
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
:format-annotations-tooltip-text="formatAnnotationsTooltipText"
- :thresholds="thresholds"
:width="width"
:height="height"
:legend-layout="legendLayout"
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index be9f104b81e..c9767330b73 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -8,10 +8,8 @@ import invalidUrl from '~/lib/utils/invalid_url';
import { ESC_KEY } from '~/lib/utils/keys';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
-import AlertsDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
import { defaultTimeRange } from '~/vue_shared/constants';
import TrackEventDirective from '~/vue_shared/directives/track_event';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { metricStates, keyboardShortcutKeys } from '../constants';
import {
timeRangeFromUrl,
@@ -30,7 +28,6 @@ import VariablesSection from './variables_section.vue';
export default {
components: {
- AlertsDeprecationWarning,
VueDraggable,
DashboardHeader,
DashboardPanel,
@@ -47,7 +44,6 @@ export default {
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
- mixins: [glFeatureFlagMixin()],
props: {
hasMetrics: {
type: Boolean,
@@ -399,8 +395,6 @@ export default {
<template>
<div class="prometheus-graphs" data-qa-selector="prometheus_graphs">
- <alerts-deprecation-warning v-if="!glFeatures.managedAlertsDeprecation" />
-
<dashboard-header
v-if="showHeader"
ref="prometheusGraphsHeader"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 446c6b52602..78e3b15913a 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -13,20 +13,16 @@ import {
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
-import { mapValues, pickBy } from 'lodash';
import { mapState } from 'vuex';
-import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import invalidUrl from '~/lib/utils/invalid_url';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import { __, n__ } from '~/locale';
import TrackEventDirective from '~/vue_shared/directives/track_event';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { panelTypes } from '../constants';
import { graphDataToCsv } from '../csv_export';
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
-import AlertWidget from './alert_widget.vue';
import MonitorAnomalyChart from './charts/anomaly.vue';
import MonitorBarChart from './charts/bar.vue';
import MonitorColumnChart from './charts/column.vue';
@@ -45,7 +41,6 @@ const events = {
export default {
components: {
MonitorEmptyChart,
- AlertWidget,
GlIcon,
GlLink,
GlLoadingIcon,
@@ -62,7 +57,6 @@ export default {
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
- mixins: [glFeatureFlagMixin()],
props: {
clipboardText: {
type: String,
@@ -84,16 +78,6 @@ export default {
required: false,
default: 'monitoringDashboard',
},
- alertsEndpoint: {
- type: String,
- required: false,
- default: null,
- },
- prometheusAlertsAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
settingsPath: {
type: String,
required: false,
@@ -104,7 +88,6 @@ export default {
return {
showTitleTooltip: false,
zoomedTimeRange: null,
- allAlerts: {},
expandBtnAvailable: Boolean(this.$listeners[events.expand]),
};
},
@@ -211,7 +194,7 @@ export default {
/**
* In monitoring, Time Series charts typically support
* a larger feature set like "annotations", "deployment
- * data", alert "thresholds" and "datazoom".
+ * data" and "datazoom".
*
* This is intentional as Time Series are more frequently
* used.
@@ -252,34 +235,11 @@ export default {
const { metrics = [] } = this.graphData;
return metrics.some(({ metricId }) => this.metricsSavedToDb.includes(metricId));
},
- alertWidgetAvailable() {
- const supportsAlerts =
- this.isPanelType(panelTypes.AREA_CHART) || this.isPanelType(panelTypes.LINE_CHART);
- return (
- supportsAlerts &&
- this.prometheusAlertsAvailable &&
- this.alertsEndpoint &&
- this.graphData &&
- this.hasMetricsInDb &&
- !this.glFeatures.managedAlertsDeprecation
- );
- },
- alertModalId() {
- return `alert-modal-${this.graphData.id}`;
- },
},
mounted() {
this.refreshTitleTooltip();
},
methods: {
- getGraphAlerts(queries) {
- if (!this.allAlerts) return {};
- const metricIdsForChart = queries.map((q) => q.metricId);
- return pickBy(this.allAlerts, (alert) => metricIdsForChart.includes(alert.metricId));
- },
- getGraphAlertValues(queries) {
- return Object.values(this.getGraphAlerts(queries));
- },
isPanelType(type) {
return this.graphData?.type === type;
},
@@ -310,24 +270,9 @@ export default {
this.onExpand();
}
},
- setAlerts(alertPath, alertAttributes) {
- if (alertAttributes) {
- this.$set(this.allAlerts, alertPath, alertAttributes);
- } else {
- this.$delete(this.allAlerts, alertPath);
- }
- },
safeUrl(url) {
return isSafeURL(url) ? url : '#';
},
- showAlertModal() {
- this.$root.$emit(BV_SHOW_MODAL, this.alertModalId);
- },
- showAlertModalFromKeyboardShortcut() {
- if (this.isContextualMenuShown) {
- this.showAlertModal();
- }
- },
visitLogsPage() {
if (this.logsPathWithTimeRange) {
visitUrl(relativePathToAbsolute(this.logsPathWithTimeRange, getBaseURL()));
@@ -348,19 +293,6 @@ export default {
this.$refs.copyChartLink.$el.firstChild.click();
}
},
- getAlertRunbooks(queries) {
- const hasRunbook = (alert) => Boolean(alert.runbookUrl);
- const graphAlertsWithRunbooks = pickBy(this.getGraphAlerts(queries), hasRunbook);
- const alertToRunbookTransform = (alert) => {
- const alertQuery = queries.find((query) => query.metricId === alert.metricId);
- return {
- key: alert.metricId,
- href: alert.runbookUrl,
- label: alertQuery.label,
- };
- };
- return mapValues(graphAlertsWithRunbooks, alertToRunbookTransform);
- },
},
panelTypes,
};
@@ -378,15 +310,6 @@ export default {
<gl-tooltip :target="() => $refs.graphTitle" :disabled="!showTitleTooltip">
{{ title }}
</gl-tooltip>
- <alert-widget
- v-if="isContextualMenuShown && alertWidgetAvailable"
- class="mx-1"
- :modal-id="alertModalId"
- :alerts-endpoint="alertsEndpoint"
- :relevant-queries="graphData.metrics"
- :alerts-to-manage="getGraphAlerts(graphData.metrics)"
- @setAlerts="setAlerts"
- />
<div class="flex-grow-1"></div>
<div v-if="graphDataIsLoading" class="mx-1 mt-1">
<gl-loading-icon size="sm" />
@@ -450,32 +373,6 @@ export default {
>
{{ __('Copy link to chart') }}
</gl-dropdown-item>
- <gl-dropdown-item
- v-if="alertWidgetAvailable"
- v-gl-modal="alertModalId"
- data-qa-selector="alert_widget_menu_item"
- >
- {{ __('Alerts') }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-for="runbook in getAlertRunbooks(graphData.metrics)"
- :key="runbook.key"
- :href="safeUrl(runbook.href)"
- data-testid="runbookLink"
- target="_blank"
- rel="noopener noreferrer"
- >
- <span class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
- <span>
- <gl-sprintf :message="s__('Metrics|View runbook - %{label}')">
- <template #label>
- {{ runbook.label }}
- </template>
- </gl-sprintf>
- </span>
- <gl-icon name="external-link" />
- </span>
- </gl-dropdown-item>
<template v-if="graphData.links && graphData.links.length">
<gl-dropdown-divider />
@@ -515,7 +412,6 @@ export default {
:deployment-data="deploymentData"
:annotations="annotations"
:project-path="projectPath"
- :thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
:timezone="dashboardTimezone"
:time-range="fixedCurrentTimeRange"
diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
index 1765a2f3d5d..a63008aa382 100644
--- a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
+++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
@@ -63,7 +63,7 @@ export default {
return !(this.form.fileName && !this.form.fileName.endsWith('.yml'));
},
fileNameFeedback() {
- return !this.fileNameState ? s__('The file name should have a .yml extension') : '';
+ return !this.fileNameState ? __('The file name should have a .yml extension') : '';
},
},
mounted() {
diff --git a/app/assets/javascripts/monitoring/monitoring_app.js b/app/assets/javascripts/monitoring/monitoring_app.js
index cf79e71b9e0..ee67e5dd827 100644
--- a/app/assets/javascripts/monitoring/monitoring_app.js
+++ b/app/assets/javascripts/monitoring/monitoring_app.js
@@ -12,10 +12,7 @@ export default (props = {}) => {
if (el && el.dataset) {
const { metricsDashboardBasePath, ...dataset } = el.dataset;
- const {
- initState,
- dataProps: { hasManagedPrometheus, ...dataProps },
- } = stateAndPropsFromDataset(dataset);
+ const { initState, dataProps } = stateAndPropsFromDataset(dataset);
const store = createStore(initState);
const router = createRouter(metricsDashboardBasePath);
@@ -24,7 +21,6 @@ export default (props = {}) => {
el,
store,
router,
- provide: { hasManagedPrometheus },
data() {
return {
dashboardProps: { ...dataProps, ...props },
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 74b777d7b44..336b613b620 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -41,7 +41,6 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
dataProps.hasMetrics = parseBoolean(dataProps.hasMetrics);
dataProps.customMetricsAvailable = parseBoolean(dataProps.customMetricsAvailable);
dataProps.prometheusAlertsAvailable = parseBoolean(dataProps.prometheusAlertsAvailable);
- dataProps.hasManagedPrometheus = parseBoolean(dataProps.hasManagedPrometheus);
return {
initState: {
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index ea3e4e5604c..a1377415efe 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -23,8 +23,8 @@ export default function initMrNotes() {
initNotesApp();
document.addEventListener('merged:UpdateActions', () => {
- initRevertCommitModal();
- initCherryPickCommitModal();
+ initRevertCommitModal('i_code_review_post_merge_submit_revert_modal');
+ initCherryPickCommitModal('i_code_review_post_merge_submit_cherry_pick_modal');
});
requestIdleCallback(() => {
diff --git a/app/assets/javascripts/mr_popover/index.js b/app/assets/javascripts/mr_popover/index.js
index 6e46c5d3c1f..714cf67e0bd 100644
--- a/app/assets/javascripts/mr_popover/index.js
+++ b/app/assets/javascripts/mr_popover/index.js
@@ -48,12 +48,7 @@ export default (elements) => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
const listenerAddedAttr = 'data-mr-listener-added';
diff --git a/app/assets/javascripts/nav/components/responsive_home.vue b/app/assets/javascripts/nav/components/responsive_home.vue
index c8f2f0bfb10..a80fda96363 100644
--- a/app/assets/javascripts/nav/components/responsive_home.vue
+++ b/app/assets/javascripts/nav/components/responsive_home.vue
@@ -55,6 +55,7 @@ export default {
v-gl-tooltip="{ title: newDropdownViewModel.title }"
:view-model="newDropdownViewModel"
class="gl-ml-3"
+ data-qa-selector="mobile_new_dropdown"
/>
</header>
<top-nav-menu-sections class="gl-h-full" :sections="menuSections" v-on="$listeners" />
diff --git a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
index 154bed81854..bfcdcfc7292 100644
--- a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
+++ b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
@@ -46,6 +46,7 @@ export default {
link-class="top-nav-menu-item"
:href="menuItem.href"
data-testid="item"
+ :data-qa-selector="`${menuItem.title.toLowerCase().replace(' ', '_')}_mobile_button`"
>
{{ menuItem.title }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 6ee5d85a09f..54fe9d19002 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -272,6 +272,8 @@ export default class BranchGraph {
return r
.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split('\n')[0])
.attr({
+ fill: 'currentColor',
+ class: 'gl-text-body',
'text-anchor': 'start',
font: '14px Monaco, monospace',
});
diff --git a/app/assets/javascripts/notebook/cells/output/latex.vue b/app/assets/javascripts/notebook/cells/output/latex.vue
index db9e61dce82..d0ed963b55d 100644
--- a/app/assets/javascripts/notebook/cells/output/latex.vue
+++ b/app/assets/javascripts/notebook/cells/output/latex.vue
@@ -1,4 +1,5 @@
<script>
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import 'mathjax/es5/tex-svg';
import Prompt from '../prompt.vue';
@@ -7,6 +8,9 @@ export default {
components: {
Prompt,
},
+ directives: {
+ SafeHtml,
+ },
props: {
count: {
type: Number,
@@ -33,13 +37,16 @@ export default {
return svg.outerHTML;
},
},
+ safeHtmlConfig: {
+ // to support SVGs and custom tags for mathjax
+ ADD_TAGS: ['use', 'mjx-container', 'mjx-tool', 'mjx-status', 'mjx-tip'],
+ },
};
</script>
<template>
<div class="output">
<prompt type="Out" :count="count" :show-output="index === 0" />
- <!-- eslint-disable -->
- <div ref="maths" v-html="code"></div>
+ <div ref="maths" v-safe-html:[$options.safeHtmlConfig]="code"></div>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 831e6dd8f92..33819c78c0f 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -78,8 +78,8 @@ export default {
v-if="resolveAllDiscussionsIssuePath && !allResolved"
v-gl-tooltip
:href="resolveAllDiscussionsIssuePath"
- :title="s__('Create issue to resolve all threads')"
- :aria-label="s__('Create issue to resolve all threads')"
+ :title="__('Create issue to resolve all threads')"
+ :aria-label="__('Create issue to resolve all threads')"
class="new-issue-for-discussion discussion-create-issue-btn"
icon="issue-new"
/>
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 6fcfa66ea49..d1df4eb848b 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -1,5 +1,6 @@
<script>
import { mapGetters, mapActions } from 'vuex';
+import { GlIntersectionObserver } from '@gitlab/ui';
import { __ } from '~/locale';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
@@ -16,7 +17,9 @@ export default {
ToggleRepliesWidget,
NoteEditedText,
DiscussionNotesRepliesWrapper,
+ GlIntersectionObserver,
},
+ inject: ['discussionObserverHandler'],
props: {
discussion: {
type: Object,
@@ -54,7 +57,11 @@ export default {
},
},
computed: {
- ...mapGetters(['userCanReply']),
+ ...mapGetters([
+ 'userCanReply',
+ 'previousUnresolvedDiscussionId',
+ 'firstUnresolvedDiscussionId',
+ ]),
hasReplies() {
return Boolean(this.replies.length);
},
@@ -77,9 +84,20 @@ export default {
url: this.discussion.discussion_path,
};
},
+ isFirstUnresolved() {
+ return this.firstUnresolvedDiscussionId === this.discussion.id;
+ },
+ },
+ observerOptions: {
+ threshold: 0,
+ rootMargin: '0px 0px -50% 0px',
},
methods: {
- ...mapActions(['toggleDiscussion', 'setSelectedCommentPositionHover']),
+ ...mapActions([
+ 'toggleDiscussion',
+ 'setSelectedCommentPositionHover',
+ 'setCurrentDiscussionId',
+ ]),
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
@@ -110,6 +128,18 @@ export default {
this.setSelectedCommentPositionHover();
}
},
+ observerTriggered(entry) {
+ this.discussionObserverHandler({
+ entry,
+ isFirstUnresolved: this.isFirstUnresolved,
+ currentDiscussion: { ...this.discussion },
+ isDiffsPage: !this.isOverviewTab,
+ functions: {
+ setCurrentDiscussionId: this.setCurrentDiscussionId,
+ getPreviousUnresolvedDiscussionId: this.previousUnresolvedDiscussionId,
+ },
+ });
+ },
},
};
</script>
@@ -122,33 +152,35 @@ export default {
@mouseleave="handleMouseLeave(discussion)"
>
<template v-if="shouldGroupReplies">
- <component
- :is="componentName(firstNote)"
- :note="componentData(firstNote)"
- :line="line || diffLine"
- :discussion-file="discussion.diff_file"
- :commit="commit"
- :help-page-path="helpPagePath"
- :show-reply-button="userCanReply"
- :discussion-root="true"
- :discussion-resolve-path="discussion.resolve_path"
- :is-overview-tab="isOverviewTab"
- @handleDeleteNote="$emit('deleteNote')"
- @startReplying="$emit('startReplying')"
- >
- <template #discussion-resolved-text>
- <note-edited-text
- v-if="discussion.resolved"
- :edited-at="discussion.resolved_at"
- :edited-by="discussion.resolved_by"
- :action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
- />
- </template>
- <template #avatar-badge>
- <slot name="avatar-badge"></slot>
- </template>
- </component>
+ <gl-intersection-observer :options="$options.observerOptions" @update="observerTriggered">
+ <component
+ :is="componentName(firstNote)"
+ :note="componentData(firstNote)"
+ :line="line || diffLine"
+ :discussion-file="discussion.diff_file"
+ :commit="commit"
+ :help-page-path="helpPagePath"
+ :show-reply-button="userCanReply"
+ :discussion-root="true"
+ :discussion-resolve-path="discussion.resolve_path"
+ :is-overview-tab="isOverviewTab"
+ @handleDeleteNote="$emit('deleteNote')"
+ @startReplying="$emit('startReplying')"
+ >
+ <template #discussion-resolved-text>
+ <note-edited-text
+ v-if="discussion.resolved"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
+ />
+ </template>
+ <template #avatar-badge>
+ <slot name="avatar-badge"></slot>
+ </template>
+ </component>
+ </gl-intersection-observer>
<discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion">
<toggle-replies-widget
v-if="hasReplies"
diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue
index 6ad565567be..1633b79c3be 100644
--- a/app/assets/javascripts/notes/components/multiline_comment_form.vue
+++ b/app/assets/javascripts/notes/components/multiline_comment_form.vue
@@ -1,6 +1,6 @@
<script>
import { GlFormSelect, GlSprintf } from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
+import { mapActions } from 'vuex';
import { getSymbol, getLineClasses } from './multiline_comment_utils';
export default {
@@ -27,13 +27,12 @@ export default {
};
},
computed: {
- ...mapState({ selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition }),
lineNumber() {
return this.commentLineOptions[this.commentLineOptions.length - 1].text;
},
},
created() {
- const line = this.selectedCommentPosition?.start || this.lineRange?.start || this.line;
+ const line = this.lineRange?.start || this.line;
this.commentLineStart = {
line_code: line.line_code,
@@ -42,7 +41,6 @@ export default {
new_line: line.new_line,
};
- if (this.selectedCommentPosition) return;
this.highlightSelection();
},
destroyed() {
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 1ce1696e332..c09582d6287 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,5 +1,6 @@
<script>
import $ from 'jquery';
+import { GlSafeHtmlDirective } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
@@ -19,6 +20,9 @@ export default {
noteForm,
Suggestions,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
mixins: [autosave],
props: {
note: {
@@ -144,6 +148,9 @@ export default {
this.removeSuggestionInfoFromBatch(suggestionId);
},
},
+ safeHtmlConfig: {
+ ADD_TAGS: ['use', 'gl-emoji'],
+ },
};
</script>
@@ -163,11 +170,7 @@ export default {
@addToBatch="addSuggestionToBatch"
@removeFromBatch="removeSuggestionFromBatch"
/>
- <div
- v-else
- class="note-text md"
- v-html="note.note_html /* eslint-disable-line vue/no-v-html */"
- ></div>
+ <div v-else v-safe-html:[$options.safeHtmlConfig]="note.note_html" class="note-text md"></div>
<note-form
v-if="isEditing"
ref="noteForm"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index b05643e5e13..d6b65ed0e8b 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,9 +1,9 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import { getDraft, updateDraft } from '~/lib/utils/autosave';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { __, sprintf } from '~/locale';
+import { __ } from '~/locale';
import markdownField from '~/vue_shared/components/markdown/field.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
@@ -17,6 +17,8 @@ export default {
markdownField,
CommentFieldLayout,
GlButton,
+ GlSprintf,
+ GlLink,
},
mixins: [glFeatureFlagsMixin(), issuableStateMixin, resolvable],
props: {
@@ -203,16 +205,12 @@ export default {
);
},
changedCommentText() {
- return sprintf(
- __(
+ return {
+ text: __(
'This comment changed after you started editing it. Review the %{startTag}updated comment%{endTag} to ensure information is not lost.',
),
- {
- startTag: `<a href="${this.noteHash}" target="_blank" rel="noopener noreferrer">`,
- endTag: '</a>',
- },
- false,
- );
+ placeholder: { link: ['startTag', 'endTag'] },
+ };
},
},
watch: {
@@ -318,11 +316,13 @@ export default {
<template>
<div ref="editNoteForm" class="note-edit-form current-note-edit-form js-discussion-note-form">
- <div
- v-if="conflictWhileEditing"
- class="js-conflict-edit-warning alert alert-danger"
- v-html="changedCommentText /* eslint-disable-line vue/no-v-html */"
- ></div>
+ <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger">
+ <gl-sprintf :message="changedCommentText.text" :placeholders="changedCommentText.placeholder">
+ <template #link="{ content }">
+ <gl-link :href="noteHash" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
<div class="flash-container timeline-content"></div>
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
<comment-field-layout
@@ -334,13 +334,13 @@ export default {
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:line="line"
+ :lines="lines"
:note="discussionNote"
:can-suggest="canSuggest"
:add-spacing-classes="false"
:help-page-path="helpPagePath"
:show-suggest-popover="showSuggestPopover"
:textarea-value="updatedNoteBody"
- :lines="lines"
@handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
>
<template #textarea>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 58570e76795..3ab3e7a20d4 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -8,6 +8,7 @@ import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import draftNote from '../../batch_comments/components/draft_note.vue';
+import { discussionIntersectionObserverHandlerFactory } from '../../diffs/utils/discussions';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
@@ -38,6 +39,9 @@ export default {
TimelineEntryItem,
},
mixins: [glFeatureFlagsMixin()],
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
props: {
noteableData: {
type: Object,
@@ -94,15 +98,17 @@ export default {
return this.noteableData.noteableType;
},
allDiscussions() {
+ let skeletonNotes = [];
+
if (this.renderSkeleton || this.isLoading) {
const prerenderedNotesCount = parseInt(this.notesData.prerenderedNotesCount, 10) || 0;
- return new Array(prerenderedNotesCount).fill({
+ skeletonNotes = new Array(prerenderedNotesCount).fill({
isSkeletonNote: true,
});
}
- return this.discussions;
+ return this.discussions.concat(skeletonNotes);
},
canReply() {
return this.userCanReply && !this.commentsDisabled && !this.timelineEnabled;
@@ -258,7 +264,13 @@ export default {
getFetchDiscussionsConfig() {
const defaultConfig = { path: this.getNotesDataByProp('discussionsPath') };
- if (doesHashExistInUrl(constants.NOTE_UNDERSCORE)) {
+ const currentFilter =
+ this.getNotesDataByProp('notesFilter') || constants.DISCUSSION_FILTERS_DEFAULT_VALUE;
+
+ if (
+ doesHashExistInUrl(constants.NOTE_UNDERSCORE) &&
+ currentFilter !== constants.DISCUSSION_FILTERS_DEFAULT_VALUE
+ ) {
return {
...defaultConfig,
filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE,
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index 96974c4fa2d..ad529eb99b6 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -1,7 +1,10 @@
import { mapGetters, mapActions, mapState } from 'vuex';
import { scrollToElementWithContext, scrollToElement } from '~/lib/utils/common_utils';
+import { updateHistory } from '../../lib/utils/url_utility';
import eventHub from '../event_hub';
+const isDiffsVirtualScrollingEnabled = () => window.gon?.features?.diffsVirtualScrolling;
+
/**
* @param {string} selector
* @returns {boolean}
@@ -11,20 +14,52 @@ function scrollTo(selector, { withoutContext = false } = {}) {
const scrollFunction = withoutContext ? scrollToElement : scrollToElementWithContext;
if (el) {
- scrollFunction(el);
+ scrollFunction(el, {
+ behavior: isDiffsVirtualScrollingEnabled() ? 'auto' : 'smooth',
+ });
return true;
}
return false;
}
+function updateUrlWithNoteId(noteId) {
+ const newHistoryEntry = {
+ state: null,
+ title: window.title,
+ url: `#note_${noteId}`,
+ replace: true,
+ };
+
+ if (noteId && isDiffsVirtualScrollingEnabled()) {
+ // Temporarily mask the ID to avoid the browser default
+ // scrolling taking over which is broken with virtual
+ // scrolling enabled.
+ const note = document.querySelector(`#note_${noteId}`);
+ note?.setAttribute('id', `masked::${note.id}`);
+
+ // Update the hash now that the ID "doesn't exist" in the page
+ updateHistory(newHistoryEntry);
+
+ // Unmask the note's ID
+ note?.setAttribute('id', `note_${noteId}`);
+ } else if (noteId) {
+ updateHistory(newHistoryEntry);
+ }
+}
+
/**
* @param {object} self Component instance with mixin applied
* @param {string} id Discussion id we are jumping to
*/
-function diffsJump({ expandDiscussion }, id) {
+function diffsJump({ expandDiscussion }, id, firstNoteId) {
const selector = `ul.notes[data-discussion-id="${id}"]`;
- eventHub.$once('scrollToDiscussion', () => scrollTo(selector));
+
+ eventHub.$once('scrollToDiscussion', () => {
+ scrollTo(selector);
+ // Wait for the discussion scroll before updating to the more specific ID
+ setTimeout(() => updateUrlWithNoteId(firstNoteId), 0);
+ });
expandDiscussion({ discussionId: id });
}
@@ -56,12 +91,13 @@ function switchToDiscussionsTabAndJumpTo(self, id) {
* @param {object} discussion Discussion we are jumping to
*/
function jumpToDiscussion(self, discussion) {
- const { id, diff_discussion: isDiffDiscussion } = discussion;
+ const { id, diff_discussion: isDiffDiscussion, notes } = discussion;
+ const firstNoteId = notes?.[0]?.id;
if (id) {
const activeTab = window.mrTabs.currentAction;
if (activeTab === 'diffs' && isDiffDiscussion) {
- diffsJump(self, id);
+ diffsJump(self, id, firstNoteId);
} else if (activeTab === 'show') {
discussionJump(self, id);
} else {
@@ -79,10 +115,18 @@ function handleDiscussionJump(self, fn, discussionId = self.currentDiscussionId)
const isDiffView = window.mrTabs.currentAction === 'diffs';
const targetId = fn(discussionId, isDiffView);
const discussion = self.getDiscussion(targetId);
+ const setHash = !isDiffView && !isDiffsVirtualScrollingEnabled();
const discussionFilePath = discussion?.diff_file?.file_path;
+ if (isDiffsVirtualScrollingEnabled()) {
+ window.location.hash = '';
+ }
+
if (discussionFilePath) {
- self.scrollToFile(discussionFilePath);
+ self.scrollToFile({
+ path: discussionFilePath,
+ setHash,
+ });
}
self.$nextTick(() => {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 7eb10f647a0..c862a29ad9c 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,4 +1,3 @@
-/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
import $ from 'jquery';
import Visibility from 'visibilityjs';
import Vue from 'vue';
@@ -71,7 +70,7 @@ export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, dat
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, discussions) =>
- commit(types.SET_INITIAL_DISCUSSIONS, discussions);
+ commit(types.ADD_OR_UPDATE_DISCUSSIONS, discussions);
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
@@ -90,14 +89,51 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi
? { params: { notes_filter: filter, persist_filter: persistFilter } }
: null;
+ if (window.gon?.features?.paginatedIssueDiscussions) {
+ return dispatch('fetchDiscussionsBatch', { path, config, perPage: 20 });
+ }
+
return axios.get(path, config).then(({ data }) => {
- commit(types.SET_INITIAL_DISCUSSIONS, data);
+ commit(types.ADD_OR_UPDATE_DISCUSSIONS, data);
commit(types.SET_FETCHING_DISCUSSIONS, false);
dispatch('updateResolvableDiscussionsCounts');
});
};
+export const fetchDiscussionsBatch = ({ commit, dispatch }, { path, config, cursor, perPage }) => {
+ const params = { ...config?.params, per_page: perPage };
+
+ if (cursor) {
+ params.cursor = cursor;
+ }
+
+ return axios.get(path, { params }).then(({ data, headers }) => {
+ commit(types.ADD_OR_UPDATE_DISCUSSIONS, data);
+
+ if (headers['x-next-page-cursor']) {
+ const nextConfig = { ...config };
+
+ if (config?.params?.persist_filter) {
+ delete nextConfig.params.notes_filter;
+ delete nextConfig.params.persist_filter;
+ }
+
+ return dispatch('fetchDiscussionsBatch', {
+ path,
+ config: nextConfig,
+ cursor: headers['x-next-page-cursor'],
+ perPage: Math.min(Math.round(perPage * 1.5), 100),
+ });
+ }
+
+ commit(types.SET_FETCHING_DISCUSSIONS, false);
+ dispatch('updateResolvableDiscussionsCounts');
+
+ return undefined;
+ });
+};
+
export const updateDiscussion = ({ commit, state }, discussion) => {
commit(types.UPDATE_DISCUSSION, discussion);
@@ -621,7 +657,7 @@ export const submitSuggestion = (
const flashMessage = errorMessage || defaultMessage;
createFlash({
- message: __(flashMessage),
+ message: flashMessage,
parent: flashContainer,
});
})
@@ -657,7 +693,7 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { message, fl
const flashMessage = errorMessage || defaultMessage;
createFlash({
- message: __(flashMessage),
+ message: flashMessage,
parent: flashContainer,
});
})
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 2e8b728e013..fcd2846ff0d 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -1,11 +1,11 @@
export const ADD_NEW_NOTE = 'ADD_NEW_NOTE';
export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
+export const ADD_OR_UPDATE_DISCUSSIONS = 'ADD_OR_UPDATE_DISCUSSIONS';
export const DELETE_NOTE = 'DELETE_NOTE';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA';
export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA';
-export const SET_INITIAL_DISCUSSIONS = 'SET_INITIAL_DISCUSSIONS';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH';
export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index c5fa34dfedd..1a99750ddb3 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -129,8 +129,8 @@ export default {
Object.assign(state, { userData: data });
},
- [types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
- const discussions = discussionsData.reduce((acc, d) => {
+ [types.ADD_OR_UPDATE_DISCUSSIONS](state, discussionsData) {
+ discussionsData.forEach((d) => {
const discussion = { ...d };
const diffData = {};
@@ -145,27 +145,38 @@ export default {
// To support legacy notes, should be very rare case.
if (discussion.individual_note && discussion.notes.length > 1) {
discussion.notes.forEach((n) => {
- acc.push({
+ const newDiscussion = {
...discussion,
...diffData,
notes: [n], // override notes array to only have one item to mimick individual_note
- });
+ };
+ const oldDiscussion = state.discussions.find(
+ (existingDiscussion) =>
+ existingDiscussion.id === discussion.id && existingDiscussion.notes[0].id === n.id,
+ );
+
+ if (oldDiscussion) {
+ state.discussions.splice(state.discussions.indexOf(oldDiscussion), 1, newDiscussion);
+ } else {
+ state.discussions.push(newDiscussion);
+ }
});
} else {
- const oldNote = utils.findNoteObjectById(state.discussions, discussion.id);
+ const oldDiscussion = utils.findNoteObjectById(state.discussions, discussion.id);
- acc.push({
- ...discussion,
- ...diffData,
- expanded: oldNote ? oldNote.expanded : discussion.expanded,
- });
+ if (oldDiscussion) {
+ state.discussions.splice(state.discussions.indexOf(oldDiscussion), 1, {
+ ...discussion,
+ ...diffData,
+ expanded: oldDiscussion.expanded,
+ });
+ } else {
+ state.discussions.push({ ...discussion, ...diffData });
+ }
}
-
- return acc;
- }, []);
-
- Object.assign(state, { discussions });
+ });
},
+
[types.SET_LAST_FETCHED_AT](state, fetchedAt) {
Object.assign(state, { lastFetchedAt: fetchedAt });
},
diff --git a/app/assets/javascripts/packages/list/components/package_search.vue b/app/assets/javascripts/packages/list/components/package_search.vue
deleted file mode 100644
index 869a2c2f641..00000000000
--- a/app/assets/javascripts/packages/list/components/package_search.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<script>
-import { mapState, mapActions } from 'vuex';
-import { s__ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
-import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
-import UrlSync from '~/vue_shared/components/url_sync.vue';
-import { sortableFields } from '../utils';
-import PackageTypeToken from './tokens/package_type_token.vue';
-
-export default {
- tokens: [
- {
- type: 'type',
- icon: 'package',
- title: s__('PackageRegistry|Type'),
- unique: true,
- token: PackageTypeToken,
- operators: OPERATOR_IS_ONLY,
- },
- ],
- components: { RegistrySearch, UrlSync },
- computed: {
- ...mapState({
- isGroupPage: (state) => state.config.isGroupPage,
- sorting: (state) => state.sorting,
- filter: (state) => state.filter,
- }),
- sortableFields() {
- return sortableFields(this.isGroupPage);
- },
- },
- methods: {
- ...mapActions(['setSorting', 'setFilter']),
- updateSorting(newValue) {
- this.setSorting(newValue);
- this.$emit('update');
- },
- },
-};
-</script>
-
-<template>
- <url-sync>
- <template #default="{ updateQuery }">
- <registry-search
- :filter="filter"
- :sorting="sorting"
- :tokens="$options.tokens"
- :sortable-fields="sortableFields"
- @sorting:changed="updateSorting"
- @filter:changed="setFilter"
- @filter:submit="$emit('update')"
- @query:changed="updateQuery"
- />
- </template>
- </url-sync>
-</template>
diff --git a/app/assets/javascripts/packages/list/components/package_title.vue b/app/assets/javascripts/packages/list/components/package_title.vue
deleted file mode 100644
index 426ad150ea9..00000000000
--- a/app/assets/javascripts/packages/list/components/package_title.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
-import { n__ } from '~/locale';
-import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '../constants';
-
-export default {
- name: 'PackageTitle',
- components: {
- TitleArea,
- MetadataItem,
- },
- props: {
- count: {
- type: Number,
- required: false,
- default: null,
- },
- helpUrl: {
- type: String,
- required: true,
- },
- },
- computed: {
- showPackageCount() {
- return Number.isInteger(this.count);
- },
- packageAmountText() {
- return n__(`%d Package`, `%d Packages`, this.count);
- },
- infoMessages() {
- return [{ text: LIST_INTRO_TEXT, link: this.helpUrl }];
- },
- },
- i18n: {
- LIST_TITLE_TEXT,
- },
-};
-</script>
-
-<template>
- <title-area :title="$options.i18n.LIST_TITLE_TEXT" :info-messages="infoMessages">
- <template #metadata-amount>
- <metadata-item v-if="showPackageCount" icon="package" :text="packageAmountText" />
- </template>
- </title-area>
-</template>
diff --git a/app/assets/javascripts/packages/list/components/packages_list_app.vue b/app/assets/javascripts/packages/list/components/packages_list_app.vue
index 4c5fb0ee7c9..31d90fa4dee 100644
--- a/app/assets/javascripts/packages/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages/list/components/packages_list_app.vue
@@ -7,6 +7,8 @@ import { s__ } from '~/locale';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
+import InfrastructureTitle from '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue';
+import InfrastructureSearch from '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
import PackageList from './packages_list.vue';
@@ -16,28 +18,10 @@ export default {
GlLink,
GlSprintf,
PackageList,
- PackageTitle: () =>
- import(/* webpackChunkName: 'package_registry_components' */ './package_title.vue'),
- PackageSearch: () =>
- import(/* webpackChunkName: 'package_registry_components' */ './package_search.vue'),
- InfrastructureTitle: () =>
- import(
- /* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue'
- ),
- InfrastructureSearch: () =>
- import(
- /* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue'
- ),
+ InfrastructureTitle,
+ InfrastructureSearch,
},
inject: {
- titleComponent: {
- from: 'titleComponent',
- default: 'PackageTitle',
- },
- searchComponent: {
- from: 'searchComponent',
- default: 'PackageSearch',
- },
emptyPageTitle: {
from: 'emptyPageTitle',
default: s__('PackageRegistry|There are no packages yet'),
@@ -111,8 +95,8 @@ export default {
<template>
<div>
- <component :is="titleComponent" :help-url="packageHelpUrl" :count="packagesCount" />
- <component :is="searchComponent" @update="requestPackagesList" />
+ <infrastructure-title :help-url="packageHelpUrl" :count="packagesCount" />
+ <infrastructure-search @update="requestPackagesList" />
<package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
<template #empty-state>
diff --git a/app/assets/javascripts/packages/list/components/tokens/package_type_token.vue b/app/assets/javascripts/packages/list/components/tokens/package_type_token.vue
deleted file mode 100644
index 74b6774712e..00000000000
--- a/app/assets/javascripts/packages/list/components/tokens/package_type_token.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import { PACKAGE_TYPES } from '../../constants';
-
-export default {
- components: {
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- },
- PACKAGE_TYPES,
-};
-</script>
-
-<template>
- <gl-filtered-search-token v-bind="{ ...$attrs }" v-on="$listeners">
- <template #suggestions>
- <gl-filtered-search-suggestion
- v-for="(type, index) in $options.PACKAGE_TYPES"
- :key="index"
- :value="type.type"
- >
- {{ type.title }}
- </gl-filtered-search-suggestion>
- </template>
- </gl-filtered-search-token>
-</template>
diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js
index 2c6fd94024e..4f5071e784b 100644
--- a/app/assets/javascripts/packages/list/constants.js
+++ b/app/assets/javascripts/packages/list/constants.js
@@ -96,10 +96,4 @@ export const PACKAGE_TYPES = [
},
];
-export const LIST_TITLE_TEXT = s__('PackageRegistry|Package Registry');
-
-export const LIST_INTRO_TEXT = s__(
- 'PackageRegistry|Publish and share packages for a variety of common package managers. %{docLinkStart}More information%{docLinkEnd}',
-);
-
export const TERRAFORM_SEARCH_TYPE = Object.freeze({ value: { data: 'terraform_module' } });
diff --git a/app/assets/javascripts/registry/explorer/components/delete_button.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_button.vue
index e4a1a1a8266..e4a1a1a8266 100644
--- a/app/assets/javascripts/registry/explorer/components/delete_button.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_button.vue
diff --git a/app/assets/javascripts/registry/explorer/components/delete_image.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_image.vue
index a313854f5e4..a313854f5e4 100644
--- a/app/assets/javascripts/registry/explorer/components/delete_image.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_image.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
index 56d2ff86fb7..56d2ff86fb7 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue
index f857c96c9d1..f857c96c9d1 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
index e9e36151fe6..e9e36151fe6 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue
index a16d95a6b30..a16d95a6b30 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue
index 12095655126..12095655126 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue
index fc1504f6c31..fc1504f6c31 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
index 3e19a646f53..3e19a646f53 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
index 0556fd298aa..0556fd298aa 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_loader.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue
index b7afa5fba33..b7afa5fba33 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_loader.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
index 1f52e319ad0..1f52e319ad0 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue
index 07ee3c6083b..07ee3c6083b 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue
index a68c4de5aa6..a68c4de5aa6 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue
index 5bd13322ebb..5bd13322ebb 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
index c1ec523574a..c1ec523574a 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue
index 5aa04419ca0..5aa04419ca0 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
index 6d2ff9ea7b6..6d2ff9ea7b6 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
diff --git a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue
index e77eda31596..e77eda31596 100644
--- a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue
diff --git a/app/assets/javascripts/registry/explorer/constants/common.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/common.js
index f7beec2c935..f7beec2c935 100644
--- a/app/assets/javascripts/registry/explorer/constants/common.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/common.js
diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
index 19e1a75fb2f..19e1a75fb2f 100644
--- a/app/assets/javascripts/registry/explorer/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
diff --git a/app/assets/javascripts/registry/explorer/constants/expiration_policies.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js
index 40f9b09a982..40f9b09a982 100644
--- a/app/assets/javascripts/registry/explorer/constants/expiration_policies.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js
diff --git a/app/assets/javascripts/registry/explorer/constants/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/index.js
index 6886356d8e2..6886356d8e2 100644
--- a/app/assets/javascripts/registry/explorer/constants/index.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/index.js
diff --git a/app/assets/javascripts/registry/explorer/constants/list.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
index d21a154d1b8..d21a154d1b8 100644
--- a/app/assets/javascripts/registry/explorer/constants/list.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
diff --git a/app/assets/javascripts/registry/explorer/constants/quick_start.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/quick_start.js
index 6a39c07eba2..6a39c07eba2 100644
--- a/app/assets/javascripts/registry/explorer/constants/quick_start.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/quick_start.js
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js
new file mode 100644
index 00000000000..9694bfd4e77
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+export const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(
+ {},
+ {
+ batchMax: 1,
+ },
+ ),
+});
diff --git a/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql
index 4c88b726ee5..4c88b726ee5 100644
--- a/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql
diff --git a/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql
index a31f2829e13..a31f2829e13 100644
--- a/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
index 01cb7fa1cab..01cb7fa1cab 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
index b5a99fd9ac1..b5a99fd9ac1 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
index a703c2dd0ac..a703c2dd0ac 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql
index 9092a71edb0..9092a71edb0 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql
diff --git a/app/assets/javascripts/registry/explorer/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
index 246a6768593..246a6768593 100644
--- a/app/assets/javascripts/registry/explorer/index.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
index feabc4f770b..feabc4f770b 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
diff --git a/app/assets/javascripts/registry/explorer/pages/index.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/index.vue
index dca63e1a569..dca63e1a569 100644
--- a/app/assets/javascripts/registry/explorer/pages/index.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/index.vue
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
index 73b957f42f2..73b957f42f2 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
diff --git a/app/assets/javascripts/registry/explorer/router.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/router.js
index a0c4417d549..a0c4417d549 100644
--- a/app/assets/javascripts/registry/explorer/router.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/router.js
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
index 73fb3656af1..71e8cf4f634 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
@@ -1,32 +1,49 @@
<script>
-import { GlAlert, GlFormGroup, GlFormInputGroup, GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
-import { __ } from '~/locale';
+import {
+ GlAlert,
+ GlFormGroup,
+ GlFormInputGroup,
+ GlSkeletonLoader,
+ GlSprintf,
+ GlEmptyState,
+} from '@gitlab/ui';
+import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
import {
DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
DEPENDENCY_PROXY_DOCS_PATH,
} from '~/packages_and_registries/settings/group/constants';
+import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
export default {
components: {
- GlFormGroup,
GlAlert,
+ GlEmptyState,
+ GlFormGroup,
GlFormInputGroup,
+ GlSkeletonLoader,
GlSprintf,
ClipboardButton,
TitleArea,
- GlSkeletonLoader,
+ ManifestsList,
},
- inject: ['groupPath', 'dependencyProxyAvailable'],
+ inject: ['groupPath', 'dependencyProxyAvailable', 'noManifestsIllustration'],
i18n: {
- proxyNotAvailableText: __('Dependency Proxy feature is limited to public groups for now.'),
- proxyDisabledText: __('Dependency Proxy disabled. To enable it, contact the group owner.'),
- proxyImagePrefix: __('Dependency Proxy image prefix'),
- copyImagePrefixText: __('Copy prefix'),
- blobCountAndSize: __('Contains %{count} blobs of images (%{size})'),
+ proxyNotAvailableText: s__(
+ 'DependencyProxy|Dependency Proxy feature is limited to public groups for now.',
+ ),
+ proxyDisabledText: s__(
+ 'DependencyProxy|Dependency Proxy disabled. To enable it, contact the group owner.',
+ ),
+ proxyImagePrefix: s__('DependencyProxy|Dependency Proxy image prefix'),
+ copyImagePrefixText: s__('DependencyProxy|Copy prefix'),
+ blobCountAndSize: s__('DependencyProxy|Contains %{count} blobs of images (%{size})'),
+ pageTitle: s__('DependencyProxy|Dependency Proxy'),
+ noManifestTitle: s__('DependencyProxy|There are no images in the cache'),
},
data() {
return {
@@ -40,7 +57,7 @@ export default {
return !this.dependencyProxyAvailable;
},
variables() {
- return { fullPath: this.groupPath };
+ return this.queryVariables;
},
},
},
@@ -56,13 +73,45 @@ export default {
dependencyProxyEnabled() {
return this.group?.dependencyProxySetting?.enabled;
},
+ queryVariables() {
+ return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE };
+ },
+ pageInfo() {
+ return this.group.dependencyProxyManifests.pageInfo;
+ },
+ manifests() {
+ return this.group.dependencyProxyManifests.nodes;
+ },
+ },
+ methods: {
+ fetchNextPage() {
+ this.fetchMore({
+ first: GRAPHQL_PAGE_SIZE,
+ after: this.pageInfo?.endCursor,
+ });
+ },
+ fetchPreviousPage() {
+ this.fetchMore({
+ first: null,
+ last: GRAPHQL_PAGE_SIZE,
+ before: this.pageInfo?.startCursor,
+ });
+ },
+ fetchMore(variables) {
+ this.$apollo.queries.group.fetchMore({
+ variables: { ...this.queryVariables, ...variables },
+ updateQuery(_, { fetchMoreResult }) {
+ return fetchMoreResult;
+ },
+ });
+ },
},
};
</script>
<template>
<div>
- <title-area :title="__('Dependency Proxy')" :info-messages="infoMessages" />
+ <title-area :title="$options.i18n.pageTitle" :info-messages="infoMessages" />
<gl-alert
v-if="!dependencyProxyAvailable"
:dismissible="false"
@@ -97,6 +146,20 @@ export default {
</span>
</template>
</gl-form-group>
+
+ <manifests-list
+ v-if="manifests && manifests.length"
+ :manifests="manifests"
+ :pagination="pageInfo"
+ @prev-page="fetchPreviousPage"
+ @next-page="fetchNextPage"
+ />
+
+ <gl-empty-state
+ v-else
+ :svg-path="noManifestsIllustration"
+ :title="$options.i18n.noManifestTitle"
+ />
</div>
<gl-alert v-else :dismissible="false" data-testid="proxy-disabled">
{{ $options.i18n.proxyDisabledText }}
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue
new file mode 100644
index 00000000000..78880b6e3f4
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { s__ } from '~/locale';
+
+export default {
+ name: 'ManifestRow',
+ components: {
+ GlSprintf,
+ ListItem,
+ TimeagoTooltip,
+ },
+ props: {
+ manifest: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ name() {
+ return this.manifest?.imageName.split(':')[0];
+ },
+ version() {
+ return this.manifest?.imageName.split(':')[1];
+ },
+ },
+ i18n: {
+ cachedAgoMessage: s__('DependencyProxy|Cached %{time}'),
+ },
+};
+</script>
+
+<template>
+ <list-item>
+ <template #left-primary> {{ name }} </template>
+ <template #left-secondary> {{ version }} </template>
+ <template #right-primary> &nbsp; </template>
+ <template #right-secondary>
+ <timeago-tooltip :time="manifest.createdAt" data-testid="cached-message">
+ <template #default="{ timeAgo }">
+ <gl-sprintf :message="$options.i18n.cachedAgoMessage">
+ <template #time>{{ timeAgo }}</template>
+ </gl-sprintf>
+ </template>
+ </timeago-tooltip>
+ </template>
+ </list-item>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue
new file mode 100644
index 00000000000..005c8feea3a
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlKeysetPagination } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import ManifestRow from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
+
+export default {
+ name: 'ManifestsLists',
+ components: {
+ ManifestRow,
+ GlKeysetPagination,
+ },
+ props: {
+ manifests: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ pagination: {
+ type: Object,
+ required: true,
+ },
+ },
+ i18n: {
+ listTitle: s__('DependencyProxy|Image list'),
+ },
+ computed: {
+ showPagination() {
+ return this.pagination.hasNextPage || this.pagination.hasPreviousPage;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-mt-6">
+ <h3 class="gl-font-base">{{ $options.i18n.listTitle }}</h3>
+ <div
+ class="gl-border-t-1 gl-border-gray-100 gl-border-t-solid gl-display-flex gl-flex-direction-column"
+ >
+ <manifest-row v-for="(manifest, index) in manifests" :key="index" :manifest="manifest" />
+ </div>
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-if="showPagination"
+ v-bind="pagination"
+ class="gl-mt-3"
+ @prev="$emit('prev-page')"
+ @next="$emit('next-page')"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
new file mode 100644
index 00000000000..3c6ede6fdce
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
@@ -0,0 +1 @@
+export const GRAPHQL_PAGE_SIZE = 20;
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js
index 16152eb81f6..56f95fa2c1f 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js
@@ -5,10 +5,5 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
index 9058d349bf3..63d5469c955 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
@@ -1,4 +1,12 @@
-query getDependencyProxyDetails($fullPath: ID!) {
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+
+query getDependencyProxyDetails(
+ $fullPath: ID!
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+) {
group(fullPath: $fullPath) {
dependencyProxyBlobCount
dependencyProxyTotalSize
@@ -6,5 +14,14 @@ query getDependencyProxyDetails($fullPath: ID!) {
dependencyProxySetting {
enabled
}
+ dependencyProxyManifests(after: $after, before: $before, first: $first, last: $last) {
+ nodes {
+ createdAt
+ imageName
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
}
}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
index 3d3fa62fd43..bcbeec72961 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
@@ -23,6 +23,7 @@ import PackageFiles from '~/packages_and_registries/package_registry/components/
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
+import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import {
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_COMPOSER,
@@ -35,12 +36,10 @@ import {
CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
SHOW_DELETE_SUCCESS_ALERT,
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
- DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
} from '~/packages_and_registries/package_registry/constants';
-import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import Tracking from '~/tracking';
@@ -62,6 +61,7 @@ export default {
AdditionalMetadata,
InstallationCommands,
PackageFiles,
+ DeletePackage,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -148,40 +148,15 @@ export default {
formatSize(size) {
return numberToHumanSize(size);
},
- async deletePackage() {
- const { data } = await this.$apollo.mutate({
- mutation: destroyPackageMutation,
- variables: {
- id: this.packageEntity.id,
- },
- });
+ navigateToListWithSuccessModal() {
+ const returnTo =
+ !this.groupListUrl || document.referrer.includes(this.projectName)
+ ? this.projectListUrl
+ : this.groupListUrl; // to avoid security issue url are supplied from backend
- if (data?.destroyPackage?.errors[0]) {
- throw data.destroyPackage.errors[0];
- }
- },
- async confirmPackageDeletion() {
- this.track(DELETE_PACKAGE_TRACKING_ACTION);
-
- try {
- await this.deletePackage();
-
- const returnTo =
- !this.groupListUrl || document.referrer.includes(this.projectName)
- ? this.projectListUrl
- : this.groupListUrl; // to avoid security issue url are supplied from backend
-
- const modalQuery = objectToQuery({ [SHOW_DELETE_SUCCESS_ALERT]: true });
+ const modalQuery = objectToQuery({ [SHOW_DELETE_SUCCESS_ALERT]: true });
- window.location.replace(`${returnTo}?${modalQuery}`);
- } catch (error) {
- createFlash({
- message: DELETE_PACKAGE_ERROR_MESSAGE,
- type: 'warning',
- captureError: true,
- error,
- });
- }
+ window.location.replace(`${returnTo}?${modalQuery}`);
},
async deletePackageFile(id) {
try {
@@ -322,26 +297,33 @@ export default {
</gl-tab>
</gl-tabs>
- <gl-modal
- ref="deleteModal"
- modal-id="delete-modal"
- data-testid="delete-modal"
- :action-primary="$options.modal.packageDeletePrimaryAction"
- :action-cancel="$options.modal.cancelAction"
- @primary="confirmPackageDeletion"
- @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE)"
+ <delete-package
+ @start="track($options.trackingActions.DELETE_PACKAGE_TRACKING_ACTION)"
+ @end="navigateToListWithSuccessModal"
>
- <template #modal-title>{{ $options.i18n.deleteModalTitle }}</template>
- <gl-sprintf :message="$options.i18n.deleteModalContent">
- <template #version>
- <strong>{{ packageEntity.version }}</strong>
- </template>
+ <template #default="{ deletePackage }">
+ <gl-modal
+ ref="deleteModal"
+ modal-id="delete-modal"
+ data-testid="delete-modal"
+ :action-primary="$options.modal.packageDeletePrimaryAction"
+ :action-cancel="$options.modal.cancelAction"
+ @primary="deletePackage(packageEntity)"
+ @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE)"
+ >
+ <template #modal-title>{{ $options.i18n.deleteModalTitle }}</template>
+ <gl-sprintf :message="$options.i18n.deleteModalContent">
+ <template #version>
+ <strong>{{ packageEntity.version }}</strong>
+ </template>
- <template #name>
- <strong>{{ packageEntity.name }}</strong>
- </template>
- </gl-sprintf>
- </gl-modal>
+ <template #name>
+ <strong>{{ packageEntity.name }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-modal>
+ </template>
+ </delete-package>
<gl-modal
ref="deleteFileModal"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue
new file mode 100644
index 00000000000..7a85fd3052e
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue
@@ -0,0 +1,62 @@
+<script>
+import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+
+import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/package_registry/constants';
+
+export default {
+ props: {
+ refetchQueries: {
+ type: Array,
+ required: false,
+ default: null,
+ },
+ showSuccessAlert: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ i18n: {
+ errorMessage: s__('PackageRegistry|Something went wrong while deleting the package.'),
+ successMessage: DELETE_PACKAGE_SUCCESS_MESSAGE,
+ },
+ methods: {
+ async deletePackage(packageEntity) {
+ try {
+ this.$emit('start');
+ const { data } = await this.$apollo.mutate({
+ mutation: destroyPackageMutation,
+ variables: {
+ id: packageEntity.id,
+ },
+ awaitRefetchQueries: Boolean(this.refetchQueries),
+ refetchQueries: this.refetchQueries,
+ });
+
+ if (data?.destroyPackage?.errors[0]) {
+ throw data.destroyPackage.errors[0];
+ }
+ if (this.showSuccessAlert) {
+ createFlash({
+ message: this.$options.i18n.successMessage,
+ type: 'success',
+ });
+ }
+ } catch (error) {
+ createFlash({
+ message: this.$options.i18n.errorMessage,
+ type: 'warning',
+ captureError: true,
+ error,
+ });
+ }
+ this.$emit('end');
+ },
+ },
+ render() {
+ return this.$scopedSlots.default({ deletePackage: this.deletePackage });
+ },
+};
+</script>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue
index 08481ac5655..11eeaf933ff 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue
@@ -1,33 +1,31 @@
<script>
-/*
- * The following component has several commented lines, this is because we are refactoring them piece by piece on several mrs
- * For a complete overview of the plan please check: https://gitlab.com/gitlab-org/gitlab/-/issues/330846
- * This work is behind feature flag: https://gitlab.com/gitlab-org/gitlab/-/issues/341136
- */
-// import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import createFlash from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
-import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
-import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
- LIST_QUERY_DEBOUNCE_TIME,
+ GRAPHQL_PAGE_SIZE,
+ DELETE_PACKAGE_SUCCESS_MESSAGE,
} from '~/packages_and_registries/package_registry/constants';
+import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
+
+import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import PackageTitle from './package_title.vue';
import PackageSearch from './package_search.vue';
-// import PackageList from './packages_list.vue';
+import PackageList from './packages_list.vue';
export default {
components: {
- // GlEmptyState,
- // GlLink,
- // GlSprintf,
- // PackageList,
+ GlEmptyState,
+ GlLink,
+ GlSprintf,
+ PackageList,
PackageTitle,
PackageSearch,
+ DeletePackage,
},
inject: [
'packageHelpUrl',
@@ -41,6 +39,7 @@ export default {
packages: {},
sort: '',
filters: {},
+ mutationLoading: false,
};
},
apollo: {
@@ -52,7 +51,9 @@ export default {
update(data) {
return data[this.graphqlResource].packages;
},
- debounce: LIST_QUERY_DEBOUNCE_TIME,
+ skip() {
+ return !this.sort;
+ },
},
},
computed: {
@@ -64,22 +65,40 @@ export default {
groupSort: this.isGroupPage ? this.sort : undefined,
packageName: this.filters?.packageName,
packageType: this.filters?.packageType,
+ first: GRAPHQL_PAGE_SIZE,
};
},
graphqlResource() {
return this.isGroupPage ? GROUP_RESOURCE_TYPE : PROJECT_RESOURCE_TYPE;
},
+ pageInfo() {
+ return this.packages?.pageInfo ?? {};
+ },
packagesCount() {
return this.packages?.count;
},
hasFilters() {
return this.filters.packageName && this.filters.packageType;
},
+ emptySearch() {
+ return !this.filters.packageName && !this.filters.packageType;
+ },
emptyStateTitle() {
return this.emptySearch
? this.$options.i18n.emptyPageTitle
: this.$options.i18n.noResultsTitle;
},
+ isLoading() {
+ return this.$apollo.queries.packages.loading || this.mutationLoading;
+ },
+ refetchQueriesData() {
+ return [
+ {
+ query: getPackagesQuery,
+ variables: this.queryVariables,
+ },
+ ];
+ },
},
mounted() {
this.checkDeleteAlert();
@@ -99,6 +118,35 @@ export default {
this.sort = sort;
this.filters = { ...filters };
},
+ updateQuery(_, { fetchMoreResult }) {
+ return fetchMoreResult;
+ },
+ fetchNextPage() {
+ const variables = {
+ ...this.queryVariables,
+ first: GRAPHQL_PAGE_SIZE,
+ last: null,
+ after: this.pageInfo?.endCursor,
+ };
+
+ this.$apollo.queries.packages.fetchMore({
+ variables,
+ updateQuery: this.updateQuery,
+ });
+ },
+ fetchPreviousPage() {
+ const variables = {
+ ...this.queryVariables,
+ first: null,
+ last: GRAPHQL_PAGE_SIZE,
+ before: this.pageInfo?.startCursor,
+ };
+
+ this.$apollo.queries.packages.fetchMore({
+ variables,
+ updateQuery: this.updateQuery,
+ });
+ },
},
i18n: {
widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
@@ -116,19 +164,35 @@ export default {
<package-title :help-url="packageHelpUrl" :count="packagesCount" />
<package-search @update="handleSearchUpdate" />
- <!-- <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
- <template #empty-state>
- <gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
- <template #description>
- <gl-sprintf v-if="hasFilters" :message="$options.i18n.widenFilters" />
- <gl-sprintf v-else :message="$options.i18n.noResultsText">
- <template #noPackagesLink="{ content }">
- <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
+ <delete-package
+ :refetch-queries="refetchQueriesData"
+ show-success-alert
+ @start="mutationLoading = true"
+ @end="mutationLoading = false"
+ >
+ <template #default="{ deletePackage }">
+ <package-list
+ :list="packages.nodes"
+ :is-loading="isLoading"
+ :page-info="pageInfo"
+ @prev-page="fetchPreviousPage"
+ @next-page="fetchNextPage"
+ @package:delete="deletePackage"
+ >
+ <template #empty-state>
+ <gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
+ <template #description>
+ <gl-sprintf v-if="hasFilters" :message="$options.i18n.widenFilters" />
+ <gl-sprintf v-else :message="$options.i18n.noResultsText">
+ <template #noPackagesLink="{ content }">
+ <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
</template>
- </gl-sprintf>
+ </gl-empty-state>
</template>
- </gl-empty-state>
+ </package-list>
</template>
- </package-list> -->
+ </delete-package>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
index 836df59ca58..3483d23e251 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
@@ -1,6 +1,6 @@
<script>
import { s__ } from '~/locale';
-import { sortableFields } from '~/packages/list/utils';
+import { sortableFields } from '~/packages_and_registries/package_registry/utils';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue
index 6e00a48586e..bf41c36e09b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue
@@ -1,6 +1,5 @@
<script>
-import { n__ } from '~/locale';
-import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '~/packages/list/constants';
+import { n__, s__ } from '~/locale';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
@@ -29,11 +28,14 @@ export default {
return n__(`%d Package`, `%d Packages`, this.count);
},
infoMessages() {
- return [{ text: LIST_INTRO_TEXT, link: this.helpUrl }];
+ return [{ text: this.$options.i18n.LIST_INTRO_TEXT, link: this.helpUrl }];
},
},
i18n: {
- LIST_TITLE_TEXT,
+ LIST_TITLE_TEXT: s__('PackageRegistry|Package Registry'),
+ LIST_INTRO_TEXT: s__(
+ 'PackageRegistry|Publish and share packages for a variety of common package managers. %{docLinkStart}More information%{docLinkEnd}',
+ ),
},
};
</script>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
index 25bac687dbf..2a946544c2f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
@@ -1,82 +1,94 @@
<script>
-import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui';
-import { mapState, mapGetters } from 'vuex';
+import { GlModal, GlSprintf, GlKeysetPagination } from '@gitlab/ui';
import { s__ } from '~/locale';
-import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
+import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
-import { TrackingActions } from '~/packages/shared/constants';
-import { packageTypeToTrackCategory } from '~/packages/shared/utils';
+import {
+ DELETE_PACKAGE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
+} from '~/packages_and_registries/package_registry/constants';
+import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils';
import Tracking from '~/tracking';
export default {
components: {
- GlPagination,
+ GlKeysetPagination,
GlModal,
GlSprintf,
PackagesListLoader,
PackagesListRow,
},
mixins: [Tracking.mixin()],
+ props: {
+ list: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ },
+
data() {
return {
itemToBeDeleted: null,
};
},
computed: {
- ...mapState({
- perPage: (state) => state.pagination.perPage,
- totalItems: (state) => state.pagination.total,
- page: (state) => state.pagination.page,
- isGroupPage: (state) => state.config.isGroupPage,
- isLoading: 'isLoading',
- }),
- ...mapGetters({ list: 'getList' }),
- currentPage: {
- get() {
- return this.page;
- },
- set(value) {
- this.$emit('page:changed', value);
- },
- },
isListEmpty() {
return !this.list || this.list.length === 0;
},
- modalAction() {
- return s__('PackageRegistry|Delete package');
- },
deletePackageName() {
return this.itemToBeDeleted?.name ?? '';
},
tracking() {
const category = this.itemToBeDeleted
- ? packageTypeToTrackCategory(this.itemToBeDeleted.package_type)
+ ? packageTypeToTrackCategory(this.itemToBeDeleted.packageType)
: undefined;
return {
category,
};
},
+ showPagination() {
+ return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
+ },
+ showDeleteModal: {
+ get() {
+ return Boolean(this.itemToBeDeleted);
+ },
+ set(value) {
+ if (!value) {
+ this.itemToBeDeleted = null;
+ }
+ },
+ },
},
methods: {
setItemToBeDeleted(item) {
this.itemToBeDeleted = { ...item };
- this.track(TrackingActions.REQUEST_DELETE_PACKAGE);
- this.$refs.packageListDeleteModal.show();
+ this.track(REQUEST_DELETE_PACKAGE_TRACKING_ACTION);
},
deleteItemConfirmation() {
this.$emit('package:delete', this.itemToBeDeleted);
- this.track(TrackingActions.DELETE_PACKAGE);
- this.itemToBeDeleted = null;
+ this.track(DELETE_PACKAGE_TRACKING_ACTION);
},
deleteItemCanceled() {
- this.track(TrackingActions.CANCEL_DELETE_PACKAGE);
- this.itemToBeDeleted = null;
+ this.track(CANCEL_DELETE_PACKAGE_TRACKING_ACTION);
},
},
i18n: {
deleteModalContent: s__(
'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
),
+ modalAction: s__('PackageRegistry|Delete package'),
},
};
</script>
@@ -95,29 +107,29 @@ export default {
v-for="packageEntity in list"
:key="packageEntity.id"
:package-entity="packageEntity"
- :package-link="packageEntity._links.web_path"
- :is-group="isGroupPage"
@packageToDelete="setItemToBeDeleted"
/>
</div>
- <gl-pagination
- v-model="currentPage"
- :per-page="perPage"
- :total-items="totalItems"
- align="center"
- class="gl-w-full gl-mt-3"
- />
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-if="showPagination"
+ v-bind="pageInfo"
+ class="gl-mt-3"
+ @prev="$emit('prev-page')"
+ @next="$emit('next-page')"
+ />
+ </div>
<gl-modal
- ref="packageListDeleteModal"
+ v-model="showDeleteModal"
modal-id="confirm-delete-pacakge"
ok-variant="danger"
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
>
- <template #modal-title>{{ modalAction }}</template>
- <template #modal-ok>{{ modalAction }}</template>
+ <template #modal-title>{{ $options.i18n.modalAction }}</template>
+ <template #modal-ok>{{ $options.i18n.modalAction }}</template>
<gl-sprintf :message="$options.i18n.deleteModalContent">
<template #name>
<strong>{{ deletePackageName }}</strong>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue
index 529a7893dfc..59354e77ee9 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import { PACKAGE_TYPES } from '~/packages/list/constants';
+import { PACKAGE_TYPES } from '~/packages_and_registries/package_registry/constants';
export default {
components: {
@@ -17,9 +17,9 @@ export default {
<gl-filtered-search-suggestion
v-for="(type, index) in $options.PACKAGE_TYPES"
:key="index"
- :value="type.type"
+ :value="type"
>
- {{ type.title }}
+ {{ type }}
</gl-filtered-search-suggestion>
</template>
</gl-filtered-search-token>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index 6a88880fa90..9fd8880861c 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
export const PACKAGE_TYPE_CONAN = 'CONAN';
export const PACKAGE_TYPE_MAVEN = 'MAVEN';
@@ -59,16 +59,7 @@ export const TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND =
export const TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND =
'copy_composer_package_include_command';
-export const TrackingCategories = {
- [PACKAGE_TYPE_MAVEN]: 'MavenPackages',
- [PACKAGE_TYPE_NPM]: 'NpmPackages',
- [PACKAGE_TYPE_CONAN]: 'ConanPackages',
-};
-
export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
-export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
- 'PackageRegistry|Something went wrong while deleting the package.',
-);
export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package file.',
);
@@ -79,6 +70,8 @@ export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__(
'PackageRegistry|Failed to load the package data',
);
+export const DELETE_PACKAGE_SUCCESS_MESSAGE = s__('PackageRegistry|Package deleted successfully');
+
export const PACKAGE_ERROR_STATUS = 'ERROR';
export const PACKAGE_DEFAULT_STATUS = 'DEFAULT';
export const PACKAGE_HIDDEN_STATUS = 'HIDDEN';
@@ -92,4 +85,52 @@ export const INSTANCE_PACKAGE_ENDPOINT_TYPE = 'instance';
export const PROJECT_RESOURCE_TYPE = 'project';
export const GROUP_RESOURCE_TYPE = 'group';
-export const LIST_QUERY_DEBOUNCE_TIME = 50;
+export const GRAPHQL_PAGE_SIZE = 20;
+
+export const LIST_KEY_NAME = 'name';
+export const LIST_KEY_PROJECT = 'project_path';
+export const LIST_KEY_VERSION = 'version';
+export const LIST_KEY_PACKAGE_TYPE = 'type';
+export const LIST_KEY_CREATED_AT = 'created_at';
+
+export const LIST_LABEL_NAME = __('Name');
+export const LIST_LABEL_PROJECT = __('Project');
+export const LIST_LABEL_VERSION = __('Version');
+export const LIST_LABEL_PACKAGE_TYPE = __('Type');
+export const LIST_LABEL_CREATED_AT = __('Published');
+
+export const SORT_FIELDS = [
+ {
+ orderBy: LIST_KEY_NAME,
+ label: LIST_LABEL_NAME,
+ },
+ {
+ orderBy: LIST_KEY_PROJECT,
+ label: LIST_LABEL_PROJECT,
+ },
+ {
+ orderBy: LIST_KEY_VERSION,
+ label: LIST_LABEL_VERSION,
+ },
+ {
+ orderBy: LIST_KEY_PACKAGE_TYPE,
+ label: LIST_LABEL_PACKAGE_TYPE,
+ },
+ {
+ orderBy: LIST_KEY_CREATED_AT,
+ label: LIST_LABEL_CREATED_AT,
+ },
+];
+
+export const PACKAGE_TYPES = [
+ s__('PackageRegistry|Composer'),
+ s__('PackageRegistry|Conan'),
+ s__('PackageRegistry|Generic'),
+ s__('PackageRegistry|Maven'),
+ s__('PackageRegistry|npm'),
+ s__('PackageRegistry|NuGet'),
+ s__('PackageRegistry|PyPI'),
+ s__('PackageRegistry|RubyGems'),
+ s__('PackageRegistry|Debian'),
+ s__('PackageRegistry|Helm'),
+];
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js b/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js
index f8cb5c516e2..21d6fbc9e1f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js
@@ -17,7 +17,6 @@ export const apolloProvider = new VueApollo({
cacheConfig: {
fragmentMatcher,
},
- assumeImmutableResults: true,
},
),
});
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
index 74e6de87866..e3115365f8b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
@@ -1,4 +1,5 @@
#import "~/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql"
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getPackages(
$fullPath: ID!
@@ -7,21 +8,47 @@ query getPackages(
$groupSort: PackageGroupSort
$packageName: String
$packageType: PackageTypeEnum
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
- packages(sort: $sort, packageName: $packageName, packageType: $packageType) {
+ packages(
+ sort: $sort
+ packageName: $packageName
+ packageType: $packageType
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ ) {
count
nodes {
...PackageData
}
+ pageInfo {
+ ...PageInfo
+ }
}
}
group(fullPath: $fullPath) @include(if: $isGroupPage) {
- packages(sort: $groupSort, packageName: $packageName, packageType: $packageType) {
+ packages(
+ sort: $groupSort
+ packageName: $packageName
+ packageType: $packageType
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ ) {
count
nodes {
...PackageData
}
+ pageInfo {
+ ...PageInfo
+ }
}
}
}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/utils.js b/app/assets/javascripts/packages_and_registries/package_registry/utils.js
index ae886952c3e..4ff8edb8f66 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/utils.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/utils.js
@@ -1,3 +1,4 @@
+import { capitalize } from 'lodash';
import { s__ } from '~/locale';
import {
PACKAGE_TYPE_CONAN,
@@ -10,6 +11,8 @@ import {
PACKAGE_TYPE_GENERIC,
PACKAGE_TYPE_DEBIAN,
PACKAGE_TYPE_HELM,
+ LIST_KEY_PROJECT,
+ SORT_FIELDS,
} from './constants';
export const getPackageTypeLabel = (packageType) => {
@@ -38,3 +41,8 @@ export const getPackageTypeLabel = (packageType) => {
return null;
}
};
+
+export const packageTypeToTrackCategory = (type) => `UI::${capitalize(type)}Packages`;
+
+export const sortableFields = (isGroupPage) =>
+ SORT_FIELDS.filter((f) => f.orderBy !== LIST_KEY_PROJECT || isGroupPage);
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
index 2dbe36def0e..5815c6393a7 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
@@ -103,6 +103,7 @@ export default {
:disabled="isLoading"
:label="$options.i18n.label"
data-qa-selector="dependency_proxy_setting_toggle"
+ data-testid="dependency-proxy-setting-toggle"
/>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js
index 16152eb81f6..56f95fa2c1f 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js
@@ -5,10 +5,5 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js b/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js
index 16152eb81f6..56f95fa2c1f 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js
@@ -5,10 +5,5 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
diff --git a/app/assets/javascripts/pages/admin/deploy_keys/index/index.js b/app/assets/javascripts/pages/admin/deploy_keys/index/index.js
new file mode 100644
index 00000000000..1e52aa3efd8
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/deploy_keys/index/index.js
@@ -0,0 +1,3 @@
+import { initAdminDeployKeysTable } from '~/admin/deploy_keys';
+
+initAdminDeployKeysTable();
diff --git a/app/assets/javascripts/pages/admin/dev_ops_report/index.js b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
index a94a60af7ff..4cad87492cf 100644
--- a/app/assets/javascripts/pages/admin/dev_ops_report/index.js
+++ b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
@@ -1,5 +1,5 @@
-import initDevOpsScore from '~/analytics/devops_report/devops_score';
-import initDevOpsScoreDisabledServicePing from '~/analytics/devops_report/devops_score_disabled_service_ping';
+import initDevOpsScore from '~/analytics/devops_reports/devops_score';
+import initDevOpsScoreDisabledServicePing from '~/analytics/devops_reports/devops_score_disabled_service_ping';
initDevOpsScoreDisabledServicePing();
initDevOpsScore();
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index 055d6f40c14..b06c804f3ca 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -46,7 +46,7 @@ export default {
return sprintf(
s__(`AdminProjects|
You’re about to permanently delete the project %{projectName}, its repository,
- and all related resources, including issues and merge requests. Once you confirm and press
+ and all related resources, including issues and merge requests. After you confirm and press
%{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`),
{
projectName: `<strong>${escape(this.projectName)}</strong>`,
@@ -70,7 +70,7 @@ export default {
},
primaryProps() {
return {
- text: s__('Delete project'),
+ text: __('Delete project'),
attributes: [{ variant: 'danger' }, { category: 'primary' }, { disabled: !this.canSubmit }],
};
},
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 946076cfb29..a1e7eb5d3de 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -1,4 +1,4 @@
-/* eslint-disable class-methods-use-this, no-unneeded-ternary */
+/* eslint-disable class-methods-use-this */
import $ from 'jquery';
import { getGroups } from '~/api/groups_api';
@@ -78,7 +78,7 @@ export default class Todos {
initDeprecatedJQueryDropdown($dropdown, {
fieldName,
selectable: true,
- filterable: searchFields ? true : false,
+ filterable: Boolean(searchFields),
search: { fields: searchFields },
data: $dropdown.data('data'),
clicked: () => {
@@ -172,8 +172,8 @@ export default class Todos {
updateBadges(data) {
$(document).trigger('todo:toggle', data.count);
- document.querySelector('.todos-pending .badge').innerHTML = addDelimiter(data.count);
- document.querySelector('.todos-done .badge').innerHTML = addDelimiter(data.done_count);
+ document.querySelector('.js-todos-pending .badge').innerHTML = addDelimiter(data.count);
+ document.querySelector('.js-todos-done .badge').innerHTML = addDelimiter(data.done_count);
}
goToTodoUrl(e) {
diff --git a/app/assets/javascripts/pages/groups/crm/contacts/index.js b/app/assets/javascripts/pages/groups/crm/contacts/index.js
new file mode 100644
index 00000000000..a595246957f
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/crm/contacts/index.js
@@ -0,0 +1,3 @@
+import initCrmContactsApp from '~/crm/contacts_bundle';
+
+initCrmContactsApp();
diff --git a/app/assets/javascripts/pages/groups/crm/organizations/index.js b/app/assets/javascripts/pages/groups/crm/organizations/index.js
new file mode 100644
index 00000000000..16479b43d52
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/crm/organizations/index.js
@@ -0,0 +1,3 @@
+import initCrmOrganizationsApp from '~/crm/organizations_bundle';
+
+initCrmOrganizationsApp();
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 49b9822795c..604da77f60c 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -10,10 +10,12 @@ import projectSelect from '~/project_select';
import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
import setupTransferEdit from '~/transfer_edit';
+import initConfirmDanger from '~/init_confirm_danger';
document.addEventListener('DOMContentLoaded', () => {
initFilePickers();
initConfirmDangerModal();
+ initConfirmDanger();
initSettingsPanels();
dirtySubmitFactory(
document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
diff --git a/app/assets/javascripts/pages/groups/new/components/app.vue b/app/assets/javascripts/pages/groups/new/components/app.vue
index 9aac364d20e..c3ac074cd7a 100644
--- a/app/assets/javascripts/pages/groups/new/components/app.vue
+++ b/app/assets/javascripts/pages/groups/new/components/app.vue
@@ -47,7 +47,7 @@ export default {
<template>
<new-namespace-page
:jump-to-last-persisted-panel="hasErrors"
- :initial-breadcrumb="s__('New group')"
+ :initial-breadcrumb="__('New group')"
:panels="$options.PANELS"
:title="s__('GroupsNew|Create new group')"
persistence-key="new_group_last_active_tab"
diff --git a/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue b/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue
index ea08a0821a8..35193171fb8 100644
--- a/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue
+++ b/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue
@@ -20,7 +20,7 @@ export default {
<gl-sprintf
:message="
s__(
- 'GroupsNew|%{linkStart}Groups%{linkEnd} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects. Groups can also be nested by creating subgroups.',
+ 'GroupsNew|%{linkStart}Groups%{linkEnd} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects.',
)
"
>
diff --git a/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js b/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js
deleted file mode 100644
index 301e0b4f7a2..00000000000
--- a/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { buildApiUrl } from '~/api/api_utils';
-import axios from '~/lib/utils/axios_utils';
-
-const NAMESPACE_EXISTS_PATH = '/api/:version/namespaces/:id/exists';
-
-export default function fetchGroupPathAvailability(groupPath, parentId) {
- const url = buildApiUrl(NAMESPACE_EXISTS_PATH).replace(':id', encodeURIComponent(groupPath));
-
- return axios.get(url, {
- params: { parent_id: parentId },
- });
-}
diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js
index c58be202043..8ce73be6e74 100644
--- a/app/assets/javascripts/pages/groups/new/group_path_validator.js
+++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js
@@ -3,7 +3,7 @@ import { debounce } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
import InputValidator from '~/validators/input_validator';
-import fetchGroupPathAvailability from './fetch_group_path_availability';
+import { getGroupPathAvailability } from '~/rest_api';
const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline';
@@ -45,7 +45,7 @@ export default class GroupPathValidator extends InputValidator {
if (inputDomElement.checkValidity() && groupPath.length > 1) {
GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector);
- fetchGroupPathAvailability(groupPath, parentId)
+ getGroupPathAvailability(groupPath, parentId)
.then(({ data }) => data)
.then((data) => {
GroupPathValidator.setInputState(inputDomElement, !data.exists);
diff --git a/app/assets/javascripts/pages/groups/packages/index/index.js b/app/assets/javascripts/pages/groups/packages/index/index.js
index 95522573b53..f9eecff4ac4 100644
--- a/app/assets/javascripts/pages/groups/packages/index/index.js
+++ b/app/assets/javascripts/pages/groups/packages/index/index.js
@@ -1,10 +1,3 @@
-(async function packageApp() {
- if (window.gon.features.packageListApollo) {
- const newPackageList = await import('~/packages_and_registries/package_registry/pages/list');
+import packageList from '~/packages_and_registries/package_registry/pages/list';
- newPackageList.default();
- } else {
- const packageList = await import('~/packages/list/packages_list_app_bundle');
- packageList.default();
- }
-})();
+packageList();
diff --git a/app/assets/javascripts/pages/groups/registry/repositories/index.js b/app/assets/javascripts/pages/groups/registry/repositories/index.js
index 6fd32321568..44579ee1217 100644
--- a/app/assets/javascripts/pages/groups/registry/repositories/index.js
+++ b/app/assets/javascripts/pages/groups/registry/repositories/index.js
@@ -1,4 +1,4 @@
-import registryExplorer from '~/registry/explorer/index';
+import registryExplorer from '~/packages_and_registries/container_registry/explorer/index';
const explorer = registryExplorer();
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index e42e89ce021..b41611001ab 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -3,7 +3,7 @@ import { GlModal } from '@gitlab/ui';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
export default {
components: {
@@ -83,7 +83,7 @@ export default {
attributes: [{ variant: 'warning' }],
},
cancelAction: {
- text: s__('Cancel'),
+ text: __('Cancel'),
attributes: [],
},
};
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 80bcbefab46..b365e039191 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -14,7 +14,7 @@ import '~/sourcegraph/load';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
const viewBlobEl = document.querySelector('#js-view-blob-app');
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 335d8d481fc..f4beefea90c 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -1,5 +1,5 @@
import { PROJECT_BADGE } from '~/badges/constants';
-import initConfirmDangerModal from '~/confirm_danger_modal';
+import initLegacyConfirmDangerModal from '~/confirm_danger_modal';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
@@ -9,11 +9,12 @@ import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
import setupTransferEdit from '~/transfer_edit';
import UserCallout from '~/user_callout';
+import initTopicsTokenSelector from '~/projects/settings/topics';
import initProjectPermissionsSettings from '../shared/permissions';
import initProjectLoadingSpinner from '../shared/save_project_loader';
initFilePickers();
-initConfirmDangerModal();
+initLegacyConfirmDangerModal();
initSettingsPanels();
initProjectDeleteButton();
mountBadgeSettings(PROJECT_BADGE);
@@ -28,3 +29,4 @@ setupTransferEdit('.js-project-transfer-form', 'select.select2');
dirtySubmitFactory(document.querySelectorAll('.js-general-settings-form, .js-mr-settings-form'));
initSearchSettings();
+initTopicsTokenSelector();
diff --git a/app/assets/javascripts/pages/projects/environments/index/index.js b/app/assets/javascripts/pages/projects/environments/index/index.js
index 554ed4f9786..f0554d64ddc 100644
--- a/app/assets/javascripts/pages/projects/environments/index/index.js
+++ b/app/assets/javascripts/pages/projects/environments/index/index.js
@@ -1,3 +1,11 @@
import initEnvironments from '~/environments/';
+import initNewEnvironments from '~/environments/new_index';
-initEnvironments();
+let el = document.getElementById('environments-list-view');
+
+if (el) {
+ initEnvironments(el);
+} else {
+ el = document.getElementById('environments-table');
+ initNewEnvironments(el);
+}
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 795ae713c08..25b62e6c971 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -382,7 +382,11 @@ export default {
:data-testid="`radio-${value}`"
>
<div>
- <gl-icon :name="icon" />
+ <gl-icon
+ data-qa-selector="fork_privacy_button"
+ :name="icon"
+ :data-qa-privacy-level="`${value}`"
+ />
<span>{{ text }}</span>
</div>
<template #help>{{ help }}</template>
diff --git a/app/assets/javascripts/pages/projects/google_cloud/index.js b/app/assets/javascripts/pages/projects/google_cloud/index.js
new file mode 100644
index 00000000000..4506ea8efd1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/google_cloud/index.js
@@ -0,0 +1,3 @@
+import initGoogleCloud from '~/google_cloud/index';
+
+initGoogleCloud();
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 62aa5df888f..24aa2f0da13 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -3,7 +3,7 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
import { IssuableType } from '~/issuable_show/constants';
import Issue from '~/issue';
-import initIncidentApp from '~/issue_show/incident';
+import { initIncidentApp, initIncidentHeaderActions } from '~/issue_show/incident';
import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue';
import { parseIssuableData } from '~/issue_show/utils/parse_data';
import initNotesApp from '~/notes';
@@ -22,16 +22,18 @@ export default function initShowIssue() {
switch (issueType) {
case IssuableType.Incident:
initIncidentApp(issuableData);
+ initIncidentHeaderActions(store);
break;
case IssuableType.Issue:
initIssuableApp(issuableData, store);
+ initIssueHeaderActions(store);
break;
default:
+ initIssueHeaderActions(store);
break;
}
initIssuableHeaderWarning(store);
- initIssueHeaderActions(store);
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
index 51980b2d971..95afcb6bda8 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
@@ -1,5 +1,6 @@
<script>
import { GlProgressBar, GlSprintf } from '@gitlab/ui';
+import eventHub from '~/invite_members/event_hub';
import { s__ } from '~/locale';
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
import LearnGitlabSectionCard from './learn_gitlab_section_card.vue';
@@ -22,6 +23,11 @@ export default {
required: true,
type: Object,
},
+ inviteMembersOpen: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
maxValue: Object.keys(ACTION_LABELS).length,
actionSections: Object.keys(ACTION_SECTIONS),
@@ -33,7 +39,15 @@ export default {
return Math.round((this.progressValue / this.$options.maxValue) * 100);
},
},
+ mounted() {
+ if (this.inviteMembersOpen) {
+ this.openInviteMembersModal('celebrate');
+ }
+ },
methods: {
+ openInviteMembersModal(mode) {
+ eventHub.$emit('openModal', { mode, inviteeType: 'members', source: 'learn-gitlab' });
+ },
actionsFor(section) {
const actions = Object.fromEntries(
Object.entries(this.actions).filter(
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
index 69fb5878f5c..0995947f3e7 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
@@ -40,6 +40,7 @@ export default {
data-track-action="click_link"
:data-track-label="$options.i18n.ACTION_LABELS[action].title"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
+ data-track-experiment="change_continuous_onboarding_link_urls"
>
{{ $options.i18n.ACTION_LABELS[action].title }}
</gl-link>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
index 6da0a8fd212..ea9eec2595f 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import LearnGitlab from '../components/learn_gitlab.vue';
@@ -11,15 +12,17 @@ function initLearnGitlab() {
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
+ const { inviteMembersOpen } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(LearnGitlab, {
- props: { actions, sections },
+ props: { actions, sections, inviteMembersOpen },
});
},
});
}
+initInviteMembersModal();
initLearnGitlab();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index dadf0988582..99094617b0a 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -28,7 +28,6 @@ export default function initMergeRequestShow() {
const el = document.querySelector('.js-mr-status-box');
const apolloProvider = new VueApollo({
- assumeImmutableResults: true,
defaultClient: createDefaultClient(),
});
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/pages/projects/packages/packages/index/index.js b/app/assets/javascripts/pages/projects/packages/packages/index/index.js
index 95522573b53..f9eecff4ac4 100644
--- a/app/assets/javascripts/pages/projects/packages/packages/index/index.js
+++ b/app/assets/javascripts/pages/projects/packages/packages/index/index.js
@@ -1,10 +1,3 @@
-(async function packageApp() {
- if (window.gon.features.packageListApollo) {
- const newPackageList = await import('~/packages_and_registries/package_registry/pages/list');
+import packageList from '~/packages_and_registries/package_registry/pages/list';
- newPackageList.default();
- } else {
- const packageList = await import('~/packages/list/packages_list_app_bundle');
- packageList.default();
- }
-})();
+packageList();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index 0e646e8c505..85443843684 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -72,18 +72,18 @@ export default {
return [
{
value: KEY_EVERY_DAY,
- text: sprintf(s__(`Every day (at %{time})`), { time: this.formattedTime }),
+ text: sprintf(__(`Every day (at %{time})`), { time: this.formattedTime }),
},
{
value: KEY_EVERY_WEEK,
- text: sprintf(s__('Every week (%{weekday} at %{time})'), {
+ text: sprintf(__('Every week (%{weekday} at %{time})'), {
weekday: this.weekday,
time: this.formattedTime,
}),
},
{
value: KEY_EVERY_MONTH,
- text: sprintf(s__('Every month (Day %{day} at %{time})'), {
+ text: sprintf(__('Every month (Day %{day} at %{time})'), {
day: this.randomDay,
time: this.formattedTime,
}),
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 9e93f709937..a26aeeb6db4 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -23,14 +23,14 @@ export default class Project {
});
}
- $('.hide-no-ssh-message').on('click', function (e) {
+ $('.js-hide-no-ssh-message').on('click', function (e) {
Cookies.set('hide_no_ssh_message', 'false');
- $(this).parents('.no-ssh-key-message').remove();
+ $(this).parents('.js-no-ssh-key-message').remove();
return e.preventDefault();
});
- $('.hide-no-password-message').on('click', function (e) {
+ $('.js-hide-no-password-message').on('click', function (e) {
Cookies.set('hide_no_password_message', 'false');
- $(this).parents('.no-password-message').remove();
+ $(this).parents('.js-no-password-message').remove();
return e.preventDefault();
});
$('.hide-auto-devops-implicitly-enabled-banner').on('click', function (e) {
diff --git a/app/assets/javascripts/pages/projects/registry/repositories/index.js b/app/assets/javascripts/pages/projects/registry/repositories/index.js
index 6fd32321568..44579ee1217 100644
--- a/app/assets/javascripts/pages/projects/registry/repositories/index.js
+++ b/app/assets/javascripts/pages/projects/registry/repositories/index.js
@@ -1,4 +1,4 @@
-import registryExplorer from '~/registry/explorer/index';
+import registryExplorer from '~/packages_and_registries/container_registry/explorer/index';
const explorer = registryExplorer();
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
index b7546a6bed7..cc92a8cd476 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
@@ -1,10 +1,5 @@
<script>
-import { GlIcon } from '@gitlab/ui';
-
export default {
- components: {
- GlIcon,
- },
props: {
label: {
type: String,
@@ -29,10 +24,14 @@ export default {
<div class="project-feature-row">
<label v-if="label" class="label-bold">
{{ label }}
- <a v-if="helpPath" :href="helpPath" target="_blank">
- <gl-icon name="question-o" />
- </a>
</label>
- <span v-if="helpText" class="form-text text-muted"> {{ helpText }} </span> <slot></slot>
+ <div>
+ <span v-if="helpText" class="text-muted"> {{ helpText }} </span>
+ <span v-if="helpPath"
+ ><a :href="helpPath" target="_blank">{{ __('Learn more') }}</a
+ >.</span
+ >
+ </div>
+ <slot></slot>
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index c53d367ed71..384ee1f5034 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -2,7 +2,7 @@
import { GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui';
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import {
visibilityOptions,
visibilityLevelDescriptions,
@@ -31,7 +31,7 @@ export default {
operationsLabel: s__('ProjectSettings|Operations'),
packagesLabel: s__('ProjectSettings|Packages'),
pagesLabel: s__('ProjectSettings|Pages'),
- ciCdLabel: s__('CI/CD'),
+ ciCdLabel: __('CI/CD'),
repositoryLabel: s__('ProjectSettings|Repository'),
requirementsLabel: s__('ProjectSettings|Requirements'),
securityAndComplianceLabel: s__('ProjectSettings|Security & Compliance'),
@@ -400,6 +400,9 @@ export default {
ref="project-visibility-settings"
:help-path="visibilityHelpPath"
:label="s__('ProjectSettings|Project visibility')"
+ :help-text="
+ s__('ProjectSettings|Manage who can see the project in the public access directory.')
+ "
>
<div class="project-feature-controls gl-display-flex gl-align-items-center gl-my-3 gl-mx-0">
<div class="select-wrapper gl-flex-grow-1">
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 78b3f2f1b30..31d69a731fe 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -7,7 +7,7 @@ import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import leaveByUrl from '~/namespaces/leave_by_url';
import initVueNotificationsDropdown from '~/notifications';
-import { initUploadFileTrigger } from '~/projects/upload_file_experiment';
+import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more';
import UserCallout from '~/user_callout';
import Star from '../../../star';
diff --git a/app/assets/javascripts/pages/projects/work_items/index/index.js b/app/assets/javascripts/pages/projects/work_items/index.js
index 11c257611f0..11c257611f0 100644
--- a/app/assets/javascripts/pages/projects/work_items/index/index.js
+++ b/app/assets/javascripts/pages/projects/work_items/index.js
diff --git a/app/assets/javascripts/pages/shared/mount_runner_instructions.js b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
index 1cb7259be64..e83c73edfde 100644
--- a/app/assets/javascripts/pages/shared/mount_runner_instructions.js
+++ b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
@@ -9,12 +9,7 @@ export function initInstallRunner(componentId = 'js-install-runner') {
const installRunnerEl = document.getElementById(componentId);
if (installRunnerEl) {
- const defaultClient = createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- );
+ const defaultClient = createDefaultClient();
const apolloProvider = new VueApollo({
defaultClient,
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index a8ec731e105..6f19a9f4379 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -12,13 +12,15 @@ import {
import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { setUrlFragment } from '~/lib/utils/url_utility';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import {
- WIKI_CONTENT_EDITOR_TRACKING_LABEL,
CONTENT_EDITOR_LOADED_ACTION,
SAVED_USING_CONTENT_EDITOR_ACTION,
+ WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ WIKI_FORMAT_LABEL,
+ WIKI_FORMAT_UPDATED_ACTION,
} from '../constants';
const trackingMixin = Tracking.mixin({
@@ -83,7 +85,7 @@ export default {
),
},
},
- feedbackTip: s__(
+ feedbackTip: __(
'Tell us your experiences with the new Markdown editor %{linkStart}in this feedback issue%{linkEnd}.',
),
},
@@ -219,6 +221,8 @@ export default {
this.trackFormSubmit();
}
+ this.trackWikiFormat();
+
// Wait until form field values are refreshed
await this.$nextTick();
@@ -304,6 +308,14 @@ export default {
}
},
+ trackWikiFormat() {
+ this.track(WIKI_FORMAT_UPDATED_ACTION, {
+ label: WIKI_FORMAT_LABEL,
+ value: this.format,
+ extra: { project_path: this.pageInfo.path, old_format: this.pageInfo.format },
+ });
+ },
+
dismissContentEditorAlert() {
this.isContentEditorAlertDismissed = true;
},
diff --git a/app/assets/javascripts/pages/shared/wikis/constants.js b/app/assets/javascripts/pages/shared/wikis/constants.js
index b358ac9cf52..94d086158f1 100644
--- a/app/assets/javascripts/pages/shared/wikis/constants.js
+++ b/app/assets/javascripts/pages/shared/wikis/constants.js
@@ -1,4 +1,5 @@
-export const WIKI_CONTENT_EDITOR_TRACKING_LABEL = 'wiki_content_editor';
-
export const CONTENT_EDITOR_LOADED_ACTION = 'content_editor_loaded';
export const SAVED_USING_CONTENT_EDITOR_ACTION = 'saved_using_content_editor';
+export const WIKI_CONTENT_EDITOR_TRACKING_LABEL = 'wiki_content_editor';
+export const WIKI_FORMAT_LABEL = 'wiki_format';
+export const WIKI_FORMAT_UPDATED_ACTION = 'wiki_format_updated';
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 0fab4678bc3..7f4e79976bc 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -5,6 +5,7 @@ import { last } from 'lodash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
+import { formatDate } from '~/lib/utils/datetime/date_format_utility';
import { n__, s__, __ } from '~/locale';
const d3 = { select };
@@ -294,7 +295,15 @@ export default class ActivityCalendar {
},
responseType: 'text',
})
- .then(({ data }) => $(this.activitiesContainer).html(data))
+ .then(({ data }) => {
+ $(this.activitiesContainer).html(data);
+ document
+ .querySelector(this.activitiesContainer)
+ .querySelectorAll('.js-localtime')
+ .forEach((el) => {
+ el.setAttribute('title', formatDate(el.getAttribute('data-datetime')));
+ });
+ })
.catch(() =>
createFlash({
message: __('An error occurred while retrieving calendar activity'),
diff --git a/app/assets/javascripts/pages/users/terms/index/index.js b/app/assets/javascripts/pages/users/terms/index/index.js
new file mode 100644
index 00000000000..29ddde6da94
--- /dev/null
+++ b/app/assets/javascripts/pages/users/terms/index/index.js
@@ -0,0 +1,4 @@
+import { initTermsApp } from '~/terms';
+import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
+
+waitForCSSLoaded(initTermsApp);
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index f163a7c3a8e..1bb82e1d8e6 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlModal, GlModalDirective, GlSegmentedControl } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import { sortOrders, sortOrderOptions } from '../constants';
import RequestWarning from './request_warning.vue';
@@ -55,7 +55,7 @@ export default {
const summary = {};
if (!this.metricDetails.summaryOptions?.hideTotal) {
- summary[s__('Total')] = this.metricDetails.calls;
+ summary[__('Total')] = this.metricDetails.calls;
}
if (!this.metricDetails.summaryOptions?.hideDuration) {
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
index f1fe8cf10fd..905a5f2d271 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
@@ -36,6 +36,11 @@ export default {
required: false,
default: false,
},
+ scrollToCommitForm: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -52,6 +57,13 @@ export default {
return !(this.message && this.targetBranch);
},
},
+ watch: {
+ scrollToCommitForm(flag) {
+ if (flag) {
+ this.scrollIntoView();
+ }
+ },
+ },
methods: {
onSubmit() {
this.$emit('submit', {
@@ -63,6 +75,10 @@ export default {
onReset() {
this.$emit('cancel');
},
+ scrollIntoView() {
+ this.$el.scrollIntoView({ behavior: 'smooth' });
+ this.$emit('scrolled-to-commit-form');
+ },
},
i18n: {
commitMessage: __('Commit message'),
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
index 0308cd9c565..14c11099756 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
@@ -10,9 +10,8 @@ import {
import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
import updateCurrentBranchMutation from '../../graphql/mutations/update_current_branch.mutation.graphql';
import updateLastCommitBranchMutation from '../../graphql/mutations/update_last_commit_branch.mutation.graphql';
+import updatePipelineEtag from '../../graphql/mutations/update_pipeline_etag.mutation.graphql';
import getCurrentBranch from '../../graphql/queries/client/current_branch.graphql';
-import getIsNewCiConfigFile from '../../graphql/queries/client/is_new_ci_config_file.graphql';
-import getPipelineEtag from '../../graphql/queries/client/pipeline_etag.graphql';
import CommitForm from './commit_form.vue';
@@ -41,18 +40,24 @@ export default {
required: false,
default: '',
},
+ isNewCiConfigFile: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ scrollToCommitForm: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
commit: {},
- isNewCiConfigFile: false,
isSaving: false,
};
},
apollo: {
- isNewCiConfigFile: {
- query: getIsNewCiConfigFile,
- },
currentBranch: {
query: getCurrentBranch,
},
@@ -96,10 +101,10 @@ export default {
content: this.ciFileContent,
lastCommitId: this.commitSha,
},
- update(store, { data }) {
+ update(_, { data }) {
const pipelineEtag = data?.commitCreate?.commit?.commitPipelinePath;
if (pipelineEtag) {
- store.writeQuery({ query: getPipelineEtag, data: { pipelineEtag } });
+ this.$apollo.mutate({ mutation: updatePipelineEtag, variables: pipelineEtag });
}
},
});
@@ -146,6 +151,8 @@ export default {
:current-branch="currentBranch"
:default-message="defaultCommitMessage"
:is-saving="isSaving"
+ :scroll-to-commit-form="scrollToCommitForm"
+ v-on="$listeners"
@cancel="onCommitCancel"
@submit="onCommitSubmit"
/>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
index ff1e0b6388f..d7594fb318a 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
@@ -2,6 +2,7 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import { experiment } from '~/experimentation/utils';
import { DRAWER_EXPANDED_KEY } from '../../constants';
import FirstPipelineCard from './cards/first_pipeline_card.vue';
import GettingStartedCard from './cards/getting_started_card.vue';
@@ -53,12 +54,23 @@ export default {
},
methods: {
setInitialExpandState() {
+ let isExpanded;
+
+ experiment('pipeline_editor_walkthrough', {
+ control: () => {
+ isExpanded = true;
+ },
+ candidate: () => {
+ isExpanded = false;
+ },
+ });
+
// We check in the local storage and if no value is defined, we want the default
// to be true. We want to explicitly set it to true here so that the drawer
// animates to open on load.
const localValue = localStorage.getItem(this.$options.localDrawerKey);
if (localValue === null) {
- this.isExpanded = true;
+ this.isExpanded = isExpanded;
}
},
setTopPosition() {
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
index 68065cc3c73..baf1d17b233 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -12,7 +12,7 @@ import { produce } from 'immer';
import { fetchPolicies } from '~/lib/graphql';
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
-import { s__ } from '~/locale';
+import { __ } from '~/locale';
import {
BRANCH_PAGINATION_LIMIT,
BRANCH_SEARCH_DEBOUNCE,
@@ -25,9 +25,9 @@ import getLastCommitBranchQuery from '~/pipeline_editor/graphql/queries/client/l
export default {
i18n: {
- dropdownHeader: s__('Switch branch'),
- title: s__('Branches'),
- fetchError: s__('Unable to fetch branch list for this project.'),
+ dropdownHeader: __('Switch branch'),
+ title: __('Branches'),
+ fetchError: __('Unable to fetch branch list for this project.'),
},
inputDebounce: BRANCH_SEARCH_DEBOUNCE,
components: {
@@ -43,14 +43,25 @@ export default {
},
inject: ['projectFullPath', 'totalBranches'],
props: {
+ hasUnsavedChanges: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
paginationLimit: {
type: Number,
required: false,
default: BRANCH_PAGINATION_LIMIT,
},
+ shouldLoadNewBranch: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
+ branchSelected: null,
availableBranches: [],
filteredBranches: [],
isSearchingBranches: false,
@@ -101,10 +112,17 @@ export default {
isBranchesLoading() {
return this.$apollo.queries.availableBranches.loading || this.isSearchingBranches;
},
- showBranchSwitcher() {
+ enableBranchSwitcher() {
return this.branches.length > 0 || this.searchTerm.length > 0;
},
},
+ watch: {
+ shouldLoadNewBranch(flag) {
+ if (flag) {
+ this.changeBranch(this.branchSelected);
+ }
+ },
+ },
methods: {
availableBranchesQueryVars(varsOverride = {}) {
if (this.searchTerm.length > 0) {
@@ -149,11 +167,7 @@ export default {
})
.catch(this.showFetchError);
},
- async selectBranch(newBranch) {
- if (newBranch === this.currentBranch) {
- return;
- }
-
+ async changeBranch(newBranch) {
this.updateCurrentBranch(newBranch);
const updatedPath = setUrlParams({ branch_name: newBranch });
historyPushState(updatedPath);
@@ -164,6 +178,19 @@ export default {
await this.$nextTick();
this.$emit('refetchContent');
},
+ selectBranch(newBranch) {
+ if (newBranch !== this.currentBranch) {
+ // If there are unsaved changes, we want to show the user
+ // a modal to confirm what to do with these before changing
+ // branches.
+ if (this.hasUnsavedChanges) {
+ this.branchSelected = newBranch;
+ this.$emit('select-branch', newBranch);
+ } else {
+ this.changeBranch(newBranch);
+ }
+ }
+ },
async setSearchTerm(newSearchTerm) {
this.pageCounter = 0;
this.searchTerm = newSearchTerm.trim();
@@ -203,11 +230,11 @@ export default {
<template>
<gl-dropdown
- v-if="showBranchSwitcher"
v-gl-tooltip.hover
:title="$options.i18n.dropdownHeader"
:header-text="$options.i18n.dropdownHeader"
:text="currentBranch"
+ :disabled="!enableBranchSwitcher"
icon="branch"
data-qa-selector="branch_selector_button"
data-testid="branch-selector"
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
index 551a0430fbf..83b074dd55c 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
+++ b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
@@ -5,10 +5,26 @@ export default {
components: {
BranchSwitcher,
},
+ props: {
+ hasUnsavedChanges: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ shouldLoadNewBranch: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
};
</script>
<template>
<div class="gl-mb-4">
- <branch-switcher v-on="$listeners" />
+ <branch-switcher
+ :has-unsaved-changes="hasUnsavedChanges"
+ :should-load-new-branch="shouldLoadNewBranch"
+ v-on="$listeners"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
index fcc31f087ff..ec6ee52b6b2 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
@@ -63,6 +63,7 @@ export default {
v-if="showPipelineStatus"
:commit-sha="commitSha"
:class="$options.pipelineStatusClasses"
+ v-on="$listeners"
/>
<validation-segment :class="validationStyling" :ci-config="ciConfigData" />
</div>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
index 75b1398a3c2..25a78aab933 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
@@ -1,17 +1,52 @@
<script>
+import { __ } from '~/locale';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
+import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
+import { PIPELINE_FAILURE } from '../../constants';
export default {
+ i18n: {
+ linkedPipelinesFetchError: __('Unable to fetch upstream and downstream pipelines.'),
+ },
components: {
PipelineMiniGraph,
+ LinkedPipelinesMiniList: () =>
+ import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
+ inject: ['projectFullPath'],
props: {
pipeline: {
type: Object,
required: true,
},
},
+ apollo: {
+ linkedPipelines: {
+ query: getLinkedPipelinesQuery,
+ variables() {
+ return {
+ fullPath: this.projectFullPath,
+ iid: this.pipeline.iid,
+ };
+ },
+ skip() {
+ return !this.pipeline.iid;
+ },
+ update({ project }) {
+ return project?.pipeline;
+ },
+ error() {
+ this.$emit('showError', {
+ type: PIPELINE_FAILURE,
+ reasons: [this.$options.i18n.linkedPipelinesFetchError],
+ });
+ },
+ },
+ },
computed: {
+ downstreamPipelines() {
+ return this.linkedPipelines?.downstream?.nodes || [];
+ },
pipelinePath() {
return this.pipeline.detailedStatus?.detailsPath || '';
},
@@ -38,12 +73,29 @@ export default {
};
});
},
+ showDownstreamPipelines() {
+ return this.downstreamPipelines.length > 0;
+ },
+ upstreamPipeline() {
+ return this.linkedPipelines?.upstream;
+ },
},
};
</script>
<template>
<div v-if="pipelineStages.length > 0" class="stage-cell gl-mr-5">
+ <linked-pipelines-mini-list
+ v-if="upstreamPipeline"
+ :triggered-by="[upstreamPipeline]"
+ data-testid="pipeline-editor-mini-graph-upstream"
+ />
<pipeline-mini-graph class="gl-display-inline" :stages="pipelineStages" />
+ <linked-pipelines-mini-list
+ v-if="showDownstreamPipelines"
+ :triggered="downstreamPipelines"
+ :pipeline-path="pipelinePath"
+ data-testid="pipeline-editor-mini-graph-downstream"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
index a1fa2147994..6fe1459c80c 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
@@ -10,7 +10,6 @@ import {
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
const POLL_INTERVAL = 10000;
@@ -21,6 +20,10 @@ export const i18n = {
`Pipeline|Pipeline %{idStart}#%{idEnd} %{statusStart}%{statusEnd} for %{commitStart}%{commitEnd}`,
),
viewBtn: s__('Pipeline|View pipeline'),
+ viewCommit: s__('Pipeline|View commit'),
+ pipelineNotTriggeredMsg: s__(
+ 'Pipeline|No pipeline was triggered for the latest changes due to the current CI/CD configuration.',
+ ),
};
export default {
@@ -34,7 +37,9 @@ export default {
GlSprintf,
PipelineEditorMiniGraph,
},
- mixins: [glFeatureFlagMixin()],
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
inject: ['projectFullPath'],
props: {
commitSha: {
@@ -59,12 +64,13 @@ export default {
};
},
update(data) {
- const { id, commitPath = '', detailedStatus = {}, stages, status } =
+ const { id, iid, commit = {}, detailedStatus = {}, stages, status } =
data.project?.pipeline || {};
return {
id,
- commitPath,
+ iid,
+ commit,
detailedStatus,
stages,
status,
@@ -73,20 +79,36 @@ export default {
result(res) {
if (res.data?.project?.pipeline) {
this.hasError = false;
+ } else {
+ this.hasError = true;
+ this.pipelineNotTriggered = true;
}
},
error() {
this.hasError = true;
+ this.networkError = true;
},
pollInterval: POLL_INTERVAL,
},
},
data() {
return {
+ networkError: false,
+ pipelineNotTriggered: false,
hasError: false,
};
},
computed: {
+ commitText() {
+ const shortSha = truncateSha(this.commitSha);
+ const commitTitle = this.pipeline.commit.title || '';
+
+ if (commitTitle.length > 0) {
+ return `${shortSha}: ${commitTitle}`;
+ }
+
+ return shortSha;
+ },
hasPipelineData() {
return Boolean(this.pipeline?.id);
},
@@ -126,13 +148,19 @@ export default {
</div>
</template>
<template v-else-if="hasError">
- <div>
+ <div v-if="networkError">
<gl-icon class="gl-mr-auto" name="warning-solid" />
<span data-testid="pipeline-error-msg">{{ $options.i18n.fetchError }}</span>
</div>
+ <div v-else>
+ <gl-icon class="gl-mr-auto" name="information-o" />
+ <span data-testid="pipeline-not-triggered-error-msg">
+ {{ $options.i18n.pipelineNotTriggeredMsg }}
+ </span>
+ </div>
</template>
<template v-else>
- <div>
+ <div class="gl-text-truncate gl-md-max-w-50p gl-mr-1">
<a :href="status.detailsPath" class="gl-mr-auto">
<ci-icon :status="status" :size="16" data-testid="pipeline-status-icon" />
</a>
@@ -144,25 +172,21 @@ export default {
<template #status>{{ status.text }}</template>
<template #commit>
<gl-link
- :href="pipeline.commitPath"
- class="commit-sha gl-font-weight-normal"
- target="_blank"
+ v-gl-tooltip.hover
+ :href="pipeline.commit.webPath"
+ :title="$options.i18n.viewCommit"
data-testid="pipeline-commit"
>
- {{ shortSha }}
+ {{ commitText }}
</gl-link>
</template>
</gl-sprintf>
</span>
</div>
<div class="gl-display-flex gl-flex-wrap">
- <pipeline-editor-mini-graph
- v-if="glFeatures.pipelineEditorMiniGraph"
- :pipeline="pipeline"
- />
+ <pipeline-editor-mini-graph :pipeline="pipeline" v-on="$listeners" />
<gl-button
class="gl-mt-2 gl-md-mt-0"
- target="_blank"
category="secondary"
variant="confirm"
:href="status.detailsPath"
diff --git a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
index 8bffd893473..611b78b3c5e 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
@@ -75,7 +75,7 @@ export default {
return this.$options.i18n.valid;
default:
// Only display first error as a reason
- return this.ciConfig?.errors.length > 0
+ return this.ciConfig?.errors?.length > 0
? sprintf(this.$options.i18n.invalidWithReason, { reason }, false)
: this.$options.i18n.invalid;
}
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index f7c9f10ea46..0cd0d17d944 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -3,15 +3,18 @@ import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { s__ } from '~/locale';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
+import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import {
CREATE_TAB,
EDITOR_APP_STATUS_EMPTY,
- EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
LINT_TAB,
MERGED_TAB,
+ TAB_QUERY_PARAM,
+ TABS_INDEX,
VISUALIZE_TAB,
} from '../constants';
import getAppStatus from '../graphql/queries/client/app_status.graphql';
@@ -20,6 +23,7 @@ import CiEditorHeader from './editor/ci_editor_header.vue';
import TextEditor from './editor/text_editor.vue';
import CiLint from './lint/ci_lint.vue';
import EditorTab from './ui/editor_tab.vue';
+import WalkthroughPopover from './walkthrough_popover.vue';
export default {
i18n: {
@@ -42,6 +46,9 @@ export default {
errorTexts: {
loadMergedYaml: s__('Pipelines|Could not load merged YAML content'),
},
+ query: {
+ TAB_QUERY_PARAM,
+ },
tabConstants: {
CREATE_TAB,
LINT_TAB,
@@ -58,6 +65,8 @@ export default {
GlTabs,
PipelineGraph,
TextEditor,
+ GitlabExperiment,
+ WalkthroughPopover,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -74,6 +83,10 @@ export default {
required: false,
default: '',
},
+ isNewCiConfigFile: {
+ type: Boolean,
+ required: true,
+ },
},
apollo: {
appStatus: {
@@ -81,9 +94,8 @@ export default {
},
},
computed: {
- hasAppError() {
- // Not an invalid config and with `mergedYaml` data missing
- return this.appStatus === EDITOR_APP_STATUS_ERROR;
+ isMergedYamlAvailable() {
+ return this.ciConfigData?.mergedYaml;
},
isEmpty() {
return this.appStatus === EDITOR_APP_STATUS_EMPTY;
@@ -98,22 +110,51 @@ export default {
return this.appStatus === EDITOR_APP_STATUS_LOADING;
},
},
+ created() {
+ const [tabQueryParam] = getParameterValues(TAB_QUERY_PARAM);
+
+ if (tabQueryParam && TABS_INDEX[tabQueryParam]) {
+ this.setDefaultTab(tabQueryParam);
+ }
+ },
methods: {
setCurrentTab(tabName) {
this.$emit('set-current-tab', tabName);
},
+ setDefaultTab(tabName) {
+ // We associate tab name with the index so that we can use tab name
+ // in other part of the app and load the corresponding tab closer to the
+ // actual component using a hash that binds the name to the indexes.
+ // This also means that if we ever changed tab order, we would justs need to
+ // update `TABS_INDEX` hash instead of all the instances in the app
+ // where we used the individual indexes
+ const newUrl = setUrlParams({ [TAB_QUERY_PARAM]: TABS_INDEX[tabName] });
+
+ this.setCurrentTab(tabName);
+ updateHistory({ url: newUrl, title: document.title, replace: true });
+ },
},
};
</script>
<template>
- <gl-tabs class="file-editor gl-mb-3">
+ <gl-tabs
+ class="file-editor gl-mb-3"
+ :query-param-name="$options.query.TAB_QUERY_PARAM"
+ sync-active-tab-with-query-params
+ >
<editor-tab
class="gl-mb-3"
+ title-link-class="js-walkthrough-popover-target"
:title="$options.i18n.tabEdit"
lazy
data-testid="editor-tab"
@click="setCurrentTab($options.tabConstants.CREATE_TAB)"
>
+ <gitlab-experiment name="pipeline_editor_walkthrough">
+ <template #candidate>
+ <walkthrough-popover v-if="isNewCiConfigFile" v-on="$listeners" />
+ </template>
+ </gitlab-experiment>
<ci-editor-header />
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
</editor-tab>
@@ -154,7 +195,7 @@ export default {
@click="setCurrentTab($options.tabConstants.MERGED_TAB)"
>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
- <gl-alert v-else-if="hasAppError" variant="danger" :dismissible="false">
+ <gl-alert v-else-if="!isMergedYamlAvailable" variant="danger" :dismissible="false">
{{ $options.errorTexts.loadMergedYaml }}
</gl-alert>
<ci-config-merged-preview v-else :ci-config-data="ciConfigData" v-on="$listeners" />
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue
index 091b202e10b..7206f19d060 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue
+++ b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue
@@ -8,6 +8,7 @@ import {
DEFAULT_FAILURE,
DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN,
+ PIPELINE_FAILURE,
} from '../../constants';
import CodeSnippetAlert from '../code_snippet_alert/code_snippet_alert.vue';
import {
@@ -24,6 +25,7 @@ export default {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
+ [PIPELINE_FAILURE]: s__('Pipelines|There was a problem with loading the pipeline data.'),
},
successTexts: {
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
@@ -74,6 +76,11 @@ export default {
text: this.$options.errorTexts[COMMIT_FAILURE],
variant: 'danger',
};
+ case PIPELINE_FAILURE:
+ return {
+ text: this.$options.errorTexts[PIPELINE_FAILURE],
+ variant: 'danger',
+ };
default:
return {
text: this.$options.errorTexts[DEFAULT_FAILURE],
diff --git a/app/assets/javascripts/pipeline_editor/components/walkthrough_popover.vue b/app/assets/javascripts/pipeline_editor/components/walkthrough_popover.vue
new file mode 100644
index 00000000000..5742b11b841
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/walkthrough_popover.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlButton, GlPopover, GlSprintf, GlOutsideDirective as Outside } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ directives: { Outside },
+ i18n: {
+ title: s__('pipelineEditorWalkthrough|See how GitLab pipelines work'),
+ description: s__(
+ 'pipelineEditorWalkthrough|This %{codeStart}.gitlab-ci.yml%{codeEnd} file creates a simple test pipeline.',
+ ),
+ instruction: s__(
+ 'pipelineEditorWalkthrough|Use the %{boldStart}commit changes%{boldEnd} button at the bottom of the page to run the pipeline.',
+ ),
+ ctaText: s__("pipelineEditorWalkthrough|Let's do this!"),
+ },
+ components: {
+ GlButton,
+ GlPopover,
+ GlSprintf,
+ },
+ data() {
+ return {
+ show: true,
+ };
+ },
+ computed: {
+ targetElement() {
+ return document.querySelector('.js-walkthrough-popover-target');
+ },
+ },
+ methods: {
+ close() {
+ this.show = false;
+ },
+ handleClickCta() {
+ this.close();
+ this.$emit('walkthrough-popover-cta-clicked');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-popover
+ :show.sync="show"
+ :title="$options.i18n.title"
+ :target="targetElement"
+ placement="right"
+ triggers="focus"
+ >
+ <div v-outside="close" class="gl-display-flex gl-flex-direction-column">
+ <p>
+ <gl-sprintf :message="$options.i18n.description">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <p>
+ <gl-sprintf :message="$options.i18n.instruction">
+ <template #bold="{ content }">
+ <strong>
+ {{ content }}
+ </strong>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <gl-button
+ class="gl-align-self-end"
+ category="tertiary"
+ data-testid="ctaBtn"
+ variant="confirm"
+ @click="handleClickCta"
+ >
+ <gl-emoji data-name="rocket" />
+ {{ this.$options.i18n.ctaText }}
+ </gl-button>
+ </div>
+ </gl-popover>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index bb03fa126a5..a2eaeeef286 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -5,24 +5,37 @@ export const CI_CONFIG_STATUS_VALID = 'VALID';
// Values for EDITOR_APP_STATUS_* are frontend specifics and
// represent the global state of the pipeline editor app.
export const EDITOR_APP_STATUS_EMPTY = 'EMPTY';
-export const EDITOR_APP_STATUS_ERROR = 'ERROR';
export const EDITOR_APP_STATUS_INVALID = CI_CONFIG_STATUS_INVALID;
export const EDITOR_APP_STATUS_LOADING = 'LOADING';
export const EDITOR_APP_STATUS_VALID = CI_CONFIG_STATUS_VALID;
+export const EDITOR_APP_VALID_STATUSES = [
+ EDITOR_APP_STATUS_EMPTY,
+ EDITOR_APP_STATUS_INVALID,
+ EDITOR_APP_STATUS_LOADING,
+ EDITOR_APP_STATUS_VALID,
+];
+
export const COMMIT_FAILURE = 'COMMIT_FAILURE';
export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
export const DEFAULT_SUCCESS = 'DEFAULT_SUCCESS';
export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
+export const PIPELINE_FAILURE = 'PIPELINE_FAILURE';
export const CREATE_TAB = 'CREATE_TAB';
export const LINT_TAB = 'LINT_TAB';
export const MERGED_TAB = 'MERGED_TAB';
export const VISUALIZE_TAB = 'VISUALIZE_TAB';
-export const TABS_WITH_COMMIT_FORM = [CREATE_TAB, LINT_TAB, VISUALIZE_TAB];
+export const TABS_INDEX = {
+ [CREATE_TAB]: '0',
+ [VISUALIZE_TAB]: '1',
+ [LINT_TAB]: '2',
+ [MERGED_TAB]: '3',
+};
+export const TAB_QUERY_PARAM = 'tab';
export const COMMIT_ACTION_CREATE = 'CREATE';
export const COMMIT_ACTION_UPDATE = 'UPDATE';
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_app_status.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_app_status.mutation.graphql
new file mode 100644
index 00000000000..7487e328668
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_app_status.mutation.graphql
@@ -0,0 +1,3 @@
+mutation updateAppStatus($appStatus: String) {
+ updateAppStatus(appStatus: $appStatus) @client
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_pipeline_etag.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_pipeline_etag.mutation.graphql
new file mode 100644
index 00000000000..9025f00b343
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_pipeline_etag.mutation.graphql
@@ -0,0 +1,3 @@
+mutation updatePipelineEtag($pipelineEtag: String) {
+ updatePipelineEtag(pipelineEtag: $pipelineEtag) @client
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/is_new_ci_config_file.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/is_new_ci_config_file.graphql
deleted file mode 100644
index 8c2ca276f50..00000000000
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/is_new_ci_config_file.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-query getIsNewCiConfigFile {
- isNewCiConfigFile @client
-}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
index 0c3653a2880..34e98ae3eb3 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
@@ -1,10 +1,13 @@
query getPipeline($fullPath: ID!, $sha: String!) {
project(fullPath: $fullPath) {
pipeline(sha: $sha) {
- commitPath
id
iid
status
+ commit {
+ title
+ webPath
+ }
detailedStatus {
detailsPath
icon
diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
index a34652b1495..e4965e00af3 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
+++ b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
@@ -1,7 +1,8 @@
-import produce from 'immer';
import axios from '~/lib/utils/axios_utils';
+import getAppStatus from './queries/client/app_status.graphql';
import getCurrentBranchQuery from './queries/client/current_branch.graphql';
import getLastCommitBranchQuery from './queries/client/last_commit_branch.query.graphql';
+import getPipelineEtag from './queries/client/pipeline_etag.graphql';
export const resolvers = {
Mutation: {
@@ -31,20 +32,28 @@ export const resolvers = {
__typename: 'CiLintContent',
}));
},
+ updateAppStatus: (_, { appStatus }, { cache }) => {
+ cache.writeQuery({
+ query: getAppStatus,
+ data: { appStatus },
+ });
+ },
updateCurrentBranch: (_, { currentBranch }, { cache }) => {
cache.writeQuery({
query: getCurrentBranchQuery,
- data: produce(cache.readQuery({ query: getCurrentBranchQuery }), (draftData) => {
- draftData.currentBranch = currentBranch;
- }),
+ data: { currentBranch },
});
},
updateLastCommitBranch: (_, { lastCommitBranch }, { cache }) => {
cache.writeQuery({
query: getLastCommitBranchQuery,
- data: produce(cache.readQuery({ query: getLastCommitBranchQuery }), (draftData) => {
- draftData.lastCommitBranch = lastCommitBranch;
- }),
+ data: { lastCommitBranch },
+ });
+ },
+ updatePipelineEtag: (_, { pipelineEtag }, { cache }) => {
+ cache.writeQuery({
+ query: getPipelineEtag,
+ data: { pipelineEtag },
});
},
},
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index 89b9091e6f9..4f7f2743aca 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -3,8 +3,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
+import { EDITOR_APP_STATUS_LOADING } from './constants';
import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
+import getAppStatus from './graphql/queries/client/app_status.graphql';
import getLastCommitBranchQuery from './graphql/queries/client/last_commit_branch.query.graphql';
import getPipelineEtag from './graphql/queries/client/pipeline_etag.graphql';
import { resolvers } from './graphql/resolvers';
@@ -59,12 +61,18 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
defaultClient: createDefaultClient(resolvers, {
typeDefs,
useGet: true,
- assumeImmutableResults: true,
}),
});
const { cache } = apolloProvider.clients.defaultClient;
cache.writeQuery({
+ query: getAppStatus,
+ data: {
+ appStatus: EDITOR_APP_STATUS_LOADING,
+ },
+ });
+
+ cache.writeQuery({
query: getCurrentBranch,
data: {
currentBranch: initialBranchName || defaultBranch,
@@ -93,6 +101,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
ciExamplesHelpPagePath,
ciHelpPagePath,
configurationPaths,
+ dataMethod: 'graphql',
defaultBranch,
emptyStateIllustrationPath,
helpPaths,
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index e70417145ab..68db5d8078f 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -12,16 +12,16 @@ import PipelineEditorMessages from './components/ui/pipeline_editor_messages.vue
import {
COMMIT_SHA_POLL_INTERVAL,
EDITOR_APP_STATUS_EMPTY,
- EDITOR_APP_STATUS_ERROR,
+ EDITOR_APP_VALID_STATUSES,
EDITOR_APP_STATUS_LOADING,
LOAD_FAILURE_UNKNOWN,
STARTER_TEMPLATE_NAME,
} from './constants';
+import updateAppStatus from './graphql/mutations/update_app_status.mutation.graphql';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
import getAppStatus from './graphql/queries/client/app_status.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
-import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql';
import getTemplate from './graphql/queries/get_starter_template.query.graphql';
import getLatestCommitShaQuery from './graphql/queries/latest_commit_sha.query.graphql';
import PipelineEditorHome from './pipeline_editor_home.vue';
@@ -44,23 +44,23 @@ export default {
},
data() {
return {
- starterTemplateName: STARTER_TEMPLATE_NAME,
ciConfigData: {},
+ currentCiFileContent: '',
failureType: null,
failureReasons: [],
initialCiFileContent: '',
isFetchingCommitSha: false,
isNewCiConfigFile: false,
lastCommittedContent: '',
- currentCiFileContent: '',
- successType: null,
+ shouldSkipStartScreen: false,
+ showFailure: false,
showStartScreen: false,
showSuccess: false,
- showFailure: false,
starterTemplate: '',
+ starterTemplateName: STARTER_TEMPLATE_NAME,
+ successType: null,
};
},
-
apollo: {
initialCiFileContent: {
fetchPolicy: fetchPolicies.NETWORK_ONLY,
@@ -103,7 +103,11 @@ export default {
}
if (!hasCIFile) {
- this.showStartScreen = true;
+ if (this.shouldSkipStartScreen) {
+ this.setNewEmptyCiConfigFile();
+ } else {
+ this.showStartScreen = true;
+ }
} else if (fileContent.length) {
// If the file content is > 0, then we make sure to reset the
// start screen flag during a refetch
@@ -141,10 +145,10 @@ export default {
return { ...ciConfig, stages };
},
result({ data }) {
- this.setAppStatus(data?.ciConfig?.status || EDITOR_APP_STATUS_ERROR);
+ this.setAppStatus(data?.ciConfig?.status);
},
- error() {
- this.reportFailure(LOAD_FAILURE_UNKNOWN);
+ error(err) {
+ this.reportFailure(LOAD_FAILURE_UNKNOWN, [String(err)]);
},
watchLoading(isLoading) {
if (isLoading) {
@@ -179,9 +183,6 @@ export default {
currentBranch: {
query: getCurrentBranch,
},
- isNewCiConfigFile: {
- query: getIsNewCiConfigFile,
- },
starterTemplate: {
query: getTemplate,
variables() {
@@ -232,6 +233,7 @@ export default {
},
mounted() {
this.loadTemplateFromURL();
+ this.checkShouldSkipStartScreen();
},
methods: {
hideFailure() {
@@ -245,8 +247,6 @@ export default {
await this.$apollo.queries.initialCiFileContent.refetch();
},
reportFailure(type, reasons = []) {
- this.setAppStatus(EDITOR_APP_STATUS_ERROR);
-
window.scrollTo({ top: 0, behavior: 'smooth' });
this.showFailure = true;
this.failureType = type;
@@ -261,12 +261,12 @@ export default {
this.currentCiFileContent = this.lastCommittedContent;
},
setAppStatus(appStatus) {
- this.$apollo.getClient().writeQuery({ query: getAppStatus, data: { appStatus } });
+ if (EDITOR_APP_VALID_STATUSES.includes(appStatus)) {
+ this.$apollo.mutate({ mutation: updateAppStatus, variables: { appStatus } });
+ }
},
setNewEmptyCiConfigFile() {
- this.$apollo
- .getClient()
- .writeQuery({ query: getIsNewCiConfigFile, data: { isNewCiConfigFile: true } });
+ this.isNewCiConfigFile = true;
this.showStartScreen = false;
},
showErrorAlert({ type, reasons = [] }) {
@@ -283,9 +283,7 @@ export default {
this.reportSuccess(type);
if (this.isNewCiConfigFile) {
- this.$apollo
- .getClient()
- .writeQuery({ query: getIsNewCiConfigFile, data: { isNewCiConfigFile: false } });
+ this.isNewCiConfigFile = false;
}
// Keep track of the latest committed content to know
@@ -300,6 +298,10 @@ export default {
this.setNewEmptyCiConfigFile();
}
},
+ checkShouldSkipStartScreen() {
+ const params = queryToObject(window.location.search);
+ this.shouldSkipStartScreen = Boolean(params?.add_new_config_file);
+ },
},
};
</script>
@@ -325,8 +327,9 @@ export default {
<pipeline-editor-home
:ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent"
- :is-new-ci-config-file="isNewCiConfigFile"
:commit-sha="commitSha"
+ :has-unsaved-changes="hasUnsavedChanges"
+ :is-new-ci-config-file="isNewCiConfigFile"
@commit="updateOnCommit"
@resetContent="resetContent"
@showError="showErrorAlert"
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
index ba567023946..8e8f31a4acc 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -1,14 +1,31 @@
<script>
+import { GlModal } from '@gitlab/ui';
+import { __ } from '~/locale';
import CommitSection from './components/commit/commit_section.vue';
import PipelineEditorDrawer from './components/drawer/pipeline_editor_drawer.vue';
import PipelineEditorFileNav from './components/file_nav/pipeline_editor_file_nav.vue';
import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
-import { TABS_WITH_COMMIT_FORM, CREATE_TAB } from './constants';
+import { CREATE_TAB } from './constants';
export default {
+ commitSectionRef: 'commitSectionRef',
+ modal: {
+ switchBranch: {
+ title: __('You have unsaved changes'),
+ body: __('Uncommitted changes will be lost if you change branches. Do you want to continue?'),
+ actionPrimary: {
+ text: __('Switch Branches'),
+ },
+ actionSecondary: {
+ text: __('Cancel'),
+ attributes: { variant: 'default' },
+ },
+ },
+ },
components: {
CommitSection,
+ GlModal,
PipelineEditorDrawer,
PipelineEditorFileNav,
PipelineEditorHeader,
@@ -28,6 +45,11 @@ export default {
required: false,
default: '',
},
+ hasUnsavedChanges: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
isNewCiConfigFile: {
type: Boolean,
required: true,
@@ -36,40 +58,82 @@ export default {
data() {
return {
currentTab: CREATE_TAB,
+ scrollToCommitForm: false,
+ shouldLoadNewBranch: false,
+ showSwitchBranchModal: false,
};
},
computed: {
showCommitForm() {
- return TABS_WITH_COMMIT_FORM.includes(this.currentTab);
+ return this.currentTab === CREATE_TAB;
},
},
methods: {
+ closeBranchModal() {
+ this.showSwitchBranchModal = false;
+ },
+ handleConfirmSwitchBranch() {
+ this.showSwitchBranchModal = true;
+ },
+ switchBranch() {
+ this.showSwitchBranchModal = false;
+ this.shouldLoadNewBranch = true;
+ },
setCurrentTab(tabName) {
this.currentTab = tabName;
},
+ setScrollToCommitForm(newValue = true) {
+ this.scrollToCommitForm = newValue;
+ },
},
};
</script>
<template>
<div class="gl-pr-9 gl-transition-medium gl-w-full">
- <pipeline-editor-file-nav v-on="$listeners" />
+ <gl-modal
+ v-if="showSwitchBranchModal"
+ visible
+ modal-id="switchBranchModal"
+ :title="$options.modal.switchBranch.title"
+ :action-primary="$options.modal.switchBranch.actionPrimary"
+ :action-secondary="$options.modal.switchBranch.actionSecondary"
+ @primary="switchBranch"
+ @secondary="closeBranchModal"
+ @cancel="closeBranchModal"
+ @hide="closeBranchModal"
+ >
+ {{ $options.modal.switchBranch.body }}
+ </gl-modal>
+ <pipeline-editor-file-nav
+ :has-unsaved-changes="hasUnsavedChanges"
+ :should-load-new-branch="shouldLoadNewBranch"
+ @select-branch="handleConfirmSwitchBranch"
+ v-on="$listeners"
+ />
<pipeline-editor-header
:ci-config-data="ciConfigData"
:commit-sha="commitSha"
:is-new-ci-config-file="isNewCiConfigFile"
+ v-on="$listeners"
/>
<pipeline-editor-tabs
:ci-config-data="ciConfigData"
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
+ :is-new-ci-config-file="isNewCiConfigFile"
v-on="$listeners"
@set-current-tab="setCurrentTab"
+ @walkthrough-popover-cta-clicked="setScrollToCommitForm"
/>
<commit-section
v-if="showCommitForm"
+ :ref="$options.commitSectionRef"
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
+ :is-new-ci-config-file="isNewCiConfigFile"
+ :scroll-to-commit-form="scrollToCommitForm"
+ @scrolled-to-commit-form="setScrollToCommitForm(false)"
v-on="$listeners"
/>
<pipeline-editor-drawer />
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
index b778fe28e59..9725e882d5e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
@@ -95,9 +95,9 @@ export default {
{{ $options.i18n.artifactsFetchErrorMessage }}
</gl-alert>
- <gl-loading-icon v-if="isLoading" size="sm" />
+ <gl-loading-icon v-else-if="isLoading" size="sm" />
- <gl-dropdown-item v-if="!artifacts.length && !isLoading" data-testid="artifacts-empty-message">
+ <gl-dropdown-item v-else-if="!artifacts.length" data-testid="artifacts-empty-message">
{{ $options.i18n.emptyArtifactsMessage }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
index 1c7c4d7c704..7d0cea67099 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
@@ -1,20 +1,15 @@
<script>
import {
- GlAlert,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
- GlLoadingIcon,
GlTooltipDirective,
} from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
-import { __, s__ } from '~/locale';
+import { __ } from '~/locale';
export const i18n = {
artifacts: __('Artifacts'),
artifactSectionHeader: __('Download artifacts'),
- artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'),
- noArtifacts: s__('Pipelines|No artifacts available'),
};
export default {
@@ -23,11 +18,9 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- GlAlert,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
- GlLoadingIcon,
},
inject: {
artifactsEndpoint: {
@@ -42,44 +35,22 @@ export default {
type: Number,
required: true,
},
- },
- data() {
- return {
- artifacts: [],
- hasError: false,
- isLoading: false,
- };
- },
- computed: {
- hasArtifacts() {
- return Boolean(this.artifacts.length);
+ artifacts: {
+ type: Array,
+ required: false,
+ default: () => [],
},
},
- methods: {
- fetchArtifacts() {
- this.isLoading = true;
- // Replace the placeholder with the ID of the pipeline we are viewing
- const endpoint = this.artifactsEndpoint.replace(
- this.artifactsEndpointPlaceholder,
- this.pipelineId,
- );
- return axios
- .get(endpoint)
- .then(({ data }) => {
- this.artifacts = data.artifacts;
- })
- .catch(() => {
- this.hasError = true;
- })
- .finally(() => {
- this.isLoading = false;
- });
+ computed: {
+ shouldShowDropdown() {
+ return this.artifacts?.length;
},
},
};
</script>
<template>
<gl-dropdown
+ v-if="shouldShowDropdown"
v-gl-tooltip
class="build-artifacts js-pipeline-dropdown-download"
:title="$options.i18n.artifacts"
@@ -89,22 +60,11 @@ export default {
right
lazy
text-sr-only
- @show.once="fetchArtifacts"
>
<gl-dropdown-section-header>{{
$options.i18n.artifactSectionHeader
}}</gl-dropdown-section-header>
- <gl-alert v-if="hasError" variant="danger" :dismissible="false">
- {{ $options.i18n.artifactsFetchErrorMessage }}
- </gl-alert>
-
- <gl-loading-icon v-if="isLoading" size="sm" />
-
- <gl-alert v-else-if="!hasArtifacts" variant="info" :dismissible="false">
- {{ $options.i18n.noArtifacts }}
- </gl-alert>
-
<gl-dropdown-item
v-for="(artifact, i) in artifacts"
:key="i"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue
index c6c81d5253b..83f6356f31a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue
@@ -76,7 +76,7 @@ export default {
</p>
<div class="row gl-mb-8">
- <div class="col-lg-3">
+ <div class="col-12">
<gl-card>
<div class="gl-flex-direction-row">
<div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="wave" /></div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
index 12ee82f0390..d64decc81ec 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTable, GlTooltipDirective } from '@gitlab/ui';
+import { GlTableLite, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../../event_hub';
import PipelineMiniGraph from './pipeline_mini_graph.vue';
@@ -18,7 +18,7 @@ const DEFAULT_TH_CLASSES =
export default {
components: {
- GlTable,
+ GlTableLite,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
PipelinesCommit,
@@ -156,7 +156,7 @@ export default {
</script>
<template>
<div class="ci-table">
- <gl-table
+ <gl-table-lite
:fields="tableFields"
:items="pipelines"
tbody-tr-class="commit"
@@ -225,7 +225,7 @@ export default {
<template #cell(actions)="{ item }">
<pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
</template>
- </gl-table>
+ </gl-table-lite>
<pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" />
</div>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index 5678b613ec6..d123f7a203c 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -49,3 +49,5 @@ export const PipelineKeyOptions = [
key: 'iid',
},
];
+
+export const TOAST_MESSAGE = s__('Pipeline|Creating pipeline.');
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
index de8de651eea..8fcae9dbad8 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
@@ -18,8 +18,11 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
}
createdAt
user {
+ id
name
+ username
webPath
+ webUrl
email
avatarUrl
status {
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
index 082d67c938c..3201f88a9e3 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
@@ -4,7 +4,7 @@ import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/commo
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import { validateParams } from '~/pipelines/utils';
-import { CANCEL_REQUEST } from '../constants';
+import { CANCEL_REQUEST, TOAST_MESSAGE } from '../constants';
import eventHub from '../event_hub';
export default {
@@ -191,7 +191,10 @@ export default {
this.service
.runMRPipeline(options)
- .then(() => this.updateTable())
+ .then(() => {
+ this.$toast.show(TOAST_MESSAGE);
+ this.updateTable();
+ })
.catch(() => {
createFlash({
message: __(
diff --git a/app/assets/javascripts/pipelines/pipeline_shared_client.js b/app/assets/javascripts/pipelines/pipeline_shared_client.js
index 7a922acd0b3..c3be487caae 100644
--- a/app/assets/javascripts/pipelines/pipeline_shared_client.js
+++ b/app/assets/javascripts/pipelines/pipeline_shared_client.js
@@ -5,7 +5,6 @@ export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
- assumeImmutableResults: true,
useGet: true,
},
),
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index 17cbcabeedb..3cb2dce87d3 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -37,7 +37,7 @@ export default {
},
primaryProps() {
return {
- text: s__('Delete account'),
+ text: __('Delete account'),
attributes: [
{ variant: 'danger', 'data-qa-selector': 'confirm_delete_account_button' },
{ category: 'primary' },
@@ -47,7 +47,7 @@ export default {
},
cancelProps() {
return {
- text: s__('Cancel'),
+ text: __('Cancel'),
};
},
canSubmit() {
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index 7917a9a75e0..45a6130826d 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -3,7 +3,7 @@ import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective }
import { escape } from 'lodash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
export default {
components: {
@@ -58,7 +58,7 @@ Please update your Git repository remotes as soon as possible.`),
},
primaryProps() {
return {
- text: s__('Update username'),
+ text: __('Update username'),
attributes: [
{ variant: 'warning' },
{ category: 'primary' },
@@ -68,7 +68,7 @@ Please update your Git repository remotes as soon as possible.`),
},
cancelProps() {
return {
- text: s__('Cancel'),
+ text: __('Cancel'),
};
},
},
diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/project_visibility.js
index e3868e2925d..1b57a69d464 100644
--- a/app/assets/javascripts/project_visibility.js
+++ b/app/assets/javascripts/project_visibility.js
@@ -1,42 +1,58 @@
import $ from 'jquery';
+import eventHub from '~/projects/new/event_hub';
-function setVisibilityOptions(namespaceSelector) {
- if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) {
- return;
- }
- const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex];
- const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset;
+// Values are from lib/gitlab/visibility_level.rb
+const visibilityLevel = {
+ private: 0,
+ internal: 10,
+ public: 20,
+};
+function setVisibilityOptions({ name, visibility, showPath, editPath }) {
document.querySelectorAll('.visibility-level-setting .form-check').forEach((option) => {
+ // Don't change anything if the option is restricted by admin
+ if (option.classList.contains('restricted')) {
+ return;
+ }
+
const optionInput = option.querySelector('input[type=radio]');
- const optionValue = optionInput ? optionInput.value : 0;
- const optionTitle = option.querySelector('.option-title');
- const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
+ const optionValue = optionInput ? parseInt(optionInput.value, 10) : 0;
- // don't change anything if the option is restricted by admin
- if (!option.classList.contains('restricted')) {
- if (visibilityLevel < optionValue) {
- option.classList.add('disabled');
- optionInput.disabled = true;
- const reason = option.querySelector('.option-disabled-reason');
- if (reason) {
- reason.innerHTML = `This project cannot be ${optionName} because the visibility of
+ if (visibilityLevel[visibility] < optionValue) {
+ option.classList.add('disabled');
+ optionInput.disabled = true;
+ const reason = option.querySelector('.option-disabled-reason');
+ if (reason) {
+ const optionTitle = option.querySelector('.option-title');
+ const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
+ reason.innerHTML = `This project cannot be ${optionName} because the visibility of
<a href="${showPath}">${name}</a> is ${visibility}. To make this project
${optionName}, you must first <a href="${editPath}">change the visibility</a>
of the parent group.`;
- }
- } else {
- option.classList.remove('disabled');
- optionInput.disabled = false;
}
+ } else {
+ option.classList.remove('disabled');
+ optionInput.disabled = false;
}
});
}
+function handleSelect2DropdownChange(namespaceSelector) {
+ if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) {
+ return;
+ }
+ const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex];
+ setVisibilityOptions(selectedNamespace.dataset);
+}
+
export default function initProjectVisibilitySelector() {
+ eventHub.$on('update-visibility', setVisibilityOptions);
+
const namespaceSelector = document.querySelector('select.js-select-namespace');
if (namespaceSelector) {
- $('.select2.js-select-namespace').on('change', () => setVisibilityOptions(namespaceSelector));
- setVisibilityOptions(namespaceSelector);
+ $('.select2.js-select-namespace').on('change', () =>
+ handleSelect2DropdownChange(namespaceSelector),
+ );
+ handleSelect2DropdownChange(namespaceSelector);
}
}
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index ec7d37644a8..f9dd72119d1 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -1,6 +1,7 @@
<script>
import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
+import api from '~/api';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import csrf from '~/lib/utils/csrf';
import eventHub from '../event_hub';
@@ -40,6 +41,11 @@ export default {
required: false,
default: false,
},
+ primaryActionEventName: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -83,6 +89,10 @@ export default {
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
handlePrimary() {
+ if (this.primaryActionEventName) {
+ api.trackRedisHllUserEvent(this.primaryActionEventName);
+ }
+
this.$refs.form.$el.submit();
},
resetModalHandler() {
diff --git a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
index 47ee8237fea..b21fd1a74de 100644
--- a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
+++ b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
@@ -9,7 +9,7 @@ import {
} from './constants';
import createStore from './store';
-export default function initInviteMembersModal() {
+export default function initInviteMembersModal(primaryActionEventName) {
const el = document.querySelector('.js-cherry-pick-commit-modal');
if (!el) {
return false;
@@ -52,6 +52,7 @@ export default function initInviteMembersModal() {
openModal: OPEN_CHERRY_PICK_MODAL,
modalId: CHERRY_PICK_MODAL_ID,
isCherryPick: true,
+ primaryActionEventName,
},
}),
});
diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
index df26aa3c830..849b2f4858c 100644
--- a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
+++ b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
@@ -10,7 +10,7 @@ import {
} from './constants';
import createStore from './store';
-export default function initInviteMembersModal() {
+export default function initInviteMembersModal(primaryActionEventName) {
const el = document.querySelector('.js-revert-commit-modal');
if (!el) {
return false;
@@ -49,6 +49,7 @@ export default function initInviteMembersModal() {
i18n: { ...I18N_REVERT_MODAL, ...I18N_MODAL },
openModal: OPEN_REVERT_MODAL,
modalId: REVERT_MODAL_ID,
+ primaryActionEventName,
},
}),
});
diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
index 2505c47147f..1d4ec4c110b 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
@@ -5,12 +5,7 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => {
diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue
index 06711e4025a..eaf93e2da4f 100644
--- a/app/assets/javascripts/projects/components/project_delete_button.vue
+++ b/app/assets/javascripts/projects/components/project_delete_button.vue
@@ -18,12 +18,36 @@ export default {
type: String,
required: true,
},
+ isFork: {
+ type: Boolean,
+ required: true,
+ },
+ issuesCount: {
+ type: Number,
+ required: true,
+ },
+ mergeRequestsCount: {
+ type: Number,
+ required: true,
+ },
+ forksCount: {
+ type: Number,
+ required: true,
+ },
+ starsCount: {
+ type: Number,
+ required: true,
+ },
},
strings: {
alertTitle: __('You are about to permanently delete this project'),
alertBody: __(
- 'Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.',
+ 'After a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.',
+ ),
+ isNotForkMessage: __(
+ 'This project is %{strongStart}NOT%{strongEnd} a fork, and has the following:',
),
+ isForkMessage: __('This forked project has the following:'),
},
};
</script>
@@ -37,6 +61,38 @@ export default {
:title="$options.strings.alertTitle"
:dismissible="false"
>
+ <p>
+ <gl-sprintf v-if="isFork" :message="$options.strings.isForkMessage" />
+ <gl-sprintf v-else :message="$options.strings.isNotForkMessage">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+ <ul>
+ <li>
+ <gl-sprintf :message="n__('%d issue', '%d issues', issuesCount)">
+ <template #issuesCount>{{ issuesCount }}</template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf
+ :message="n__('%d merge requests', '%d merge requests', mergeRequestsCount)"
+ >
+ <template #mergeRequestsCount>{{ mergeRequestsCount }}</template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="n__('%d fork', '%d forks', forksCount)">
+ <template #forksCount>{{ forksCount }}</template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="n__('%d star', '%d stars', starsCount)">
+ <template #starsCount>{{ starsCount }}</template>
+ </gl-sprintf>
+ </li>
+ </ul>
<gl-sprintf :message="$options.strings.alertBody">
<template #strong="{ content }">
<strong>{{ content }}</strong>
diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js
index 2da9449d24e..0393d82ca36 100644
--- a/app/assets/javascripts/projects/default_project_templates.js
+++ b/app/assets/javascripts/projects/default_project_templates.js
@@ -93,6 +93,10 @@ export default {
text: s__('ProjectTemplates|Serverless Framework/JS'),
icon: '.template-option .icon-serverless_framework',
},
+ tencent_serverless_framework: {
+ text: s__('ProjectTemplates|Tencent Serverless Framework/NextjsSSR'),
+ icon: '.template-option .icon-tencent_serverless_framework',
+ },
cluster_management: {
text: s__('ProjectTemplates|GitLab Cluster Management'),
icon: '.template-option .icon-cluster_management',
diff --git a/app/assets/javascripts/projects/details/upload_button.vue b/app/assets/javascripts/projects/details/upload_button.vue
index 5b19f15c233..e1c8c66a214 100644
--- a/app/assets/javascripts/projects/details/upload_button.vue
+++ b/app/assets/javascripts/projects/details/upload_button.vue
@@ -1,7 +1,6 @@
<script>
import { GlButton, GlModalDirective } from '@gitlab/ui';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
-import { trackFileUploadEvent } from '../upload_file_experiment_tracking';
const UPLOAD_BLOB_MODAL_ID = 'details-modal-upload-blob';
@@ -30,11 +29,6 @@ export default {
default: '',
},
},
- methods: {
- trackOpenModal() {
- trackFileUploadEvent('click_upload_modal_trigger');
- },
- },
uploadBlobModalId: UPLOAD_BLOB_MODAL_ID,
};
</script>
@@ -44,7 +38,6 @@ export default {
v-gl-modal="$options.uploadBlobModalId"
icon="upload"
data-testid="upload-file-button"
- @click="trackOpenModal"
>{{ __('Upload File') }}</gl-button
>
<upload-blob-modal
diff --git a/app/assets/javascripts/projects/new/components/app.vue b/app/assets/javascripts/projects/new/components/app.vue
index 6e9efc50be8..476d6466cbb 100644
--- a/app/assets/javascripts/projects/new/components/app.vue
+++ b/app/assets/javascripts/projects/new/components/app.vue
@@ -95,7 +95,7 @@ export default {
<template>
<new-namespace-page
- :initial-breadcrumb="s__('New project')"
+ :initial-breadcrumb="__('New project')"
:panels="availablePanels"
:jump-to-last-persisted-panel="hasErrors"
:title="s__('ProjectsNew|Create new project')"
diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
index bf44ff70562..e0ba60074af 100644
--- a/app/assets/javascripts/projects/new/components/new_project_url_select.vue
+++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
@@ -6,9 +6,9 @@ import {
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
- GlLoadingIcon,
GlSearchBoxByType,
} from '@gitlab/ui';
+import { joinPaths } from '~/lib/utils/url_utility';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
@@ -24,7 +24,6 @@ export default {
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
- GlLoadingIcon,
GlSearchBoxByType,
},
mixins: [Tracking.mixin()],
@@ -103,6 +102,15 @@ export default {
focusInput() {
this.$refs.search.focusInput();
},
+ handleDropdownItemClick(namespace) {
+ eventHub.$emit('update-visibility', {
+ name: namespace.name,
+ visibility: namespace.visibility,
+ showPath: namespace.webUrl,
+ editPath: joinPaths(namespace.webUrl, '-', 'edit'),
+ });
+ this.setNamespace(namespace);
+ },
handleSelectTemplate(groupId) {
this.groupToFilterBy = this.userGroups.find(
(group) => getIdFromGraphQLId(group.id) === groupId,
@@ -134,23 +142,23 @@ export default {
<gl-search-box-by-type
ref="search"
v-model.trim="search"
+ :is-loading="$apollo.queries.currentUser.loading"
data-qa-selector="select_namespace_dropdown_search_field"
/>
- <gl-loading-icon v-if="$apollo.queries.currentUser.loading" />
- <template v-else>
+ <template v-if="!$apollo.queries.currentUser.loading">
<template v-if="hasGroupMatches">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="group of filteredGroups"
:key="group.id"
- @click="setNamespace(group)"
+ @click="handleDropdownItemClick(group)"
>
{{ group.fullPath }}
</gl-dropdown-item>
</template>
<template v-if="hasNamespaceMatches">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
- <gl-dropdown-item @click="setNamespace(userNamespace)">
+ <gl-dropdown-item @click="handleDropdownItemClick(userNamespace)">
{{ userNamespace.fullPath }}
</gl-dropdown-item>
</template>
@@ -158,6 +166,11 @@ export default {
</template>
</gl-dropdown>
- <input type="hidden" name="project[namespace_id]" :value="selectedNamespace.id" />
+ <input
+ id="project_namespace_id"
+ type="hidden"
+ name="project[namespace_id]"
+ :value="selectedNamespace.id"
+ />
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/projects/new/index.js b/app/assets/javascripts/projects/new/index.js
index 572d3276e4f..010c6a29ae3 100644
--- a/app/assets/javascripts/projects/new/index.js
+++ b/app/assets/javascripts/projects/new/index.js
@@ -50,7 +50,7 @@ export function initNewProjectUrlSelect() {
new Vue({
el,
apolloProvider: new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
}),
provide: {
namespaceFullPath: el.dataset.namespaceFullPath,
diff --git a/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
index e16fe5dde49..74febec5a51 100644
--- a/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
+++ b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
@@ -4,6 +4,9 @@ query searchNamespacesWhereUserCanCreateProjects($search: String) {
nodes {
id
fullPath
+ name
+ visibility
+ webUrl
}
}
namespace {
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 25bacc1cc4a..7379d5caed7 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -11,12 +11,17 @@ export default {
DeploymentFrequencyCharts: () =>
import('ee_component/dora/components/deployment_frequency_charts.vue'),
LeadTimeCharts: () => import('ee_component/dora/components/lead_time_charts.vue'),
+ ProjectQualitySummary: () => import('ee_component/project_quality_summary/app.vue'),
},
inject: {
shouldRenderDoraCharts: {
type: Boolean,
default: false,
},
+ shouldRenderQualitySummary: {
+ type: Boolean,
+ default: false,
+ },
},
data() {
return {
@@ -31,6 +36,10 @@ export default {
chartsToShow.push('deployment-frequency', 'lead-time');
}
+ if (this.shouldRenderQualitySummary) {
+ chartsToShow.push('project-quality');
+ }
+
return chartsToShow;
},
},
@@ -68,6 +77,9 @@ export default {
<lead-time-charts />
</gl-tab>
</template>
+ <gl-tab v-if="shouldRenderQualitySummary" :title="s__('QualitySummary|Project quality')">
+ <project-quality-summary />
+ </gl-tab>
</gl-tabs>
<pipeline-charts v-else />
</div>
diff --git a/app/assets/javascripts/projects/pipelines/charts/index.js b/app/assets/javascripts/projects/pipelines/charts/index.js
index f7ea89068a0..003b61d94b1 100644
--- a/app/assets/javascripts/projects/pipelines/charts/index.js
+++ b/app/assets/javascripts/projects/pipelines/charts/index.js
@@ -7,13 +7,14 @@ import ProjectPipelinesCharts from './components/app.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
const mountPipelineChartsApp = (el) => {
const { projectPath } = el.dataset;
const shouldRenderDoraCharts = parseBoolean(el.dataset.shouldRenderDoraCharts);
+ const shouldRenderQualitySummary = parseBoolean(el.dataset.shouldRenderQualitySummary);
return new Vue({
el,
@@ -25,6 +26,7 @@ const mountPipelineChartsApp = (el) => {
provide: {
projectPath,
shouldRenderDoraCharts,
+ shouldRenderQualitySummary,
},
render: (createElement) => createElement(ProjectPipelinesCharts, {}),
});
diff --git a/app/assets/javascripts/projects/project_delete_button.js b/app/assets/javascripts/projects/project_delete_button.js
index aa7fc31d307..b4d388eda3a 100644
--- a/app/assets/javascripts/projects/project_delete_button.js
+++ b/app/assets/javascripts/projects/project_delete_button.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import ProjectDeleteButton from './components/project_delete_button.vue';
export default (selector = '#js-project-delete-button') => {
@@ -6,7 +7,15 @@ export default (selector = '#js-project-delete-button') => {
if (!el) return;
- const { confirmPhrase, formPath } = el.dataset;
+ const {
+ confirmPhrase,
+ formPath,
+ isFork,
+ issuesCount,
+ mergeRequestsCount,
+ forksCount,
+ starsCount,
+ } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
@@ -16,6 +25,11 @@ export default (selector = '#js-project-delete-button') => {
props: {
confirmPhrase,
formPath,
+ isFork: parseBoolean(isFork),
+ issuesCount: parseInt(issuesCount, 10),
+ mergeRequestsCount: parseInt(mergeRequestsCount, 10),
+ forksCount: parseInt(forksCount, 10),
+ starsCount: parseInt(starsCount, 10),
},
});
},
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index b350db0c838..8d71a3dab68 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -43,6 +43,8 @@ const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingPr
};
const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => {
+ const specialRepo = document.querySelector('.js-user-readme-repo');
+
// eslint-disable-next-line @gitlab/no-global-event-off
$projectNameInput.off('keyup change').on('keyup change', () => {
onProjectNameChange($projectNameInput, $projectPathInput);
@@ -54,6 +56,11 @@ const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => {
$projectPathInput.off('keyup change').on('keyup change', () => {
onProjectPathChange($projectNameInput, $projectPathInput, hasUserDefinedProjectName);
hasUserDefinedProjectPath = $projectPathInput.val().trim().length > 0;
+
+ specialRepo.classList.toggle(
+ 'gl-display-none',
+ $projectPathInput.val() !== $projectPathInput.data('username'),
+ );
});
};
diff --git a/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
new file mode 100644
index 00000000000..e8b0e95b142
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
@@ -0,0 +1,92 @@
+<script>
+import { GlTokenSelector, GlAvatarLabeled } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import searchProjectTopics from '../queries/project_topics_search.query.graphql';
+
+export default {
+ components: {
+ GlTokenSelector,
+ GlAvatarLabeled,
+ },
+ i18n: {
+ placeholder: s__('ProjectSettings|Search for topic'),
+ },
+ props: {
+ selected: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ apollo: {
+ topics: {
+ query: searchProjectTopics,
+ variables() {
+ return {
+ search: this.search,
+ };
+ },
+ update(data) {
+ return (
+ data.topics?.nodes.filter(
+ (topic) => !this.selectedTokens.some((token) => token.name === topic.name),
+ ) || []
+ );
+ },
+ debounce: 250,
+ },
+ },
+ data() {
+ return {
+ topics: [],
+ selectedTokens: this.selected,
+ search: '',
+ };
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.topics.loading;
+ },
+ placeholderText() {
+ return this.selectedTokens.length ? '' : this.$options.i18n.placeholder;
+ },
+ },
+ methods: {
+ handleEnter(event) {
+ // Prevent form from submitting when adding a token
+ if (event.target.value !== '') {
+ event.preventDefault();
+ }
+ },
+ filterTopics(searchTerm) {
+ this.search = searchTerm;
+ },
+ onTokensUpdate(tokens) {
+ this.$emit('update', tokens);
+ },
+ },
+};
+</script>
+<template>
+ <gl-token-selector
+ ref="tokenSelector"
+ v-model="selectedTokens"
+ :dropdown-items="topics"
+ :loading="loading"
+ allow-user-defined-tokens
+ :placeholder="placeholderText"
+ @keydown.enter="handleEnter"
+ @text-input="filterTopics"
+ @input="onTokensUpdate"
+ >
+ <template #dropdown-item-content="{ dropdownItem }">
+ <gl-avatar-labeled
+ :src="dropdownItem.avatarUrl"
+ :entity-name="dropdownItem.name"
+ :label="dropdownItem.name"
+ :size="32"
+ shape="rect"
+ />
+ </template>
+ </gl-token-selector>
+</template>
diff --git a/app/assets/javascripts/projects/settings/topics/index.js b/app/assets/javascripts/projects/settings/topics/index.js
new file mode 100644
index 00000000000..3fbd1a61abe
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/topics/index.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import TopicsTokenSelector from './components/topics_token_selector.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default () => {
+ const el = document.querySelector('.js-topics-selector');
+
+ if (!el) return null;
+
+ const { hiddenInputId } = el.dataset;
+ const hiddenInput = document.getElementById(hiddenInputId);
+
+ const selected = hiddenInput.value
+ ? hiddenInput.value.split(/,\s*/).map((token, index) => ({
+ id: index,
+ name: token,
+ }))
+ : [];
+
+ return new Vue({
+ el,
+ apolloProvider,
+ render(createElement) {
+ return createElement(TopicsTokenSelector, {
+ props: {
+ selected,
+ },
+ on: {
+ update(tokens) {
+ const value = tokens.map(({ name }) => name).join(', ');
+ hiddenInput.value = value;
+ // Dispatch `input` event so form submit button becomes active
+ hiddenInput.dispatchEvent(
+ new Event('input', {
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
+ },
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql b/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql
new file mode 100644
index 00000000000..b193165062a
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql
@@ -0,0 +1,9 @@
+query searchProjectTopics($search: String) {
+ topics(search: $search) {
+ nodes {
+ id
+ name
+ avatarUrl
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index 4c083ed5496..14c8c53dd19 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -31,6 +31,9 @@ export default {
selectedTemplate: {
default: '',
},
+ selectedFileTemplateProjectId: {
+ default: null,
+ },
outgoingName: {
default: '',
},
@@ -80,7 +83,7 @@ export default {
});
},
- onSaveTemplate({ selectedTemplate, outgoingName, projectKey }) {
+ onSaveTemplate({ selectedTemplate, fileTemplateProjectId, outgoingName, projectKey }) {
this.isTemplateSaving = true;
const body = {
@@ -88,6 +91,7 @@ export default {
outgoing_name: outgoingName,
project_key: projectKey,
service_desk_enabled: this.isEnabled,
+ file_template_project_id: fileTemplateProjectId,
};
return axios
@@ -132,6 +136,7 @@ export default {
:custom-email="updatedCustomEmail"
:custom-email-enabled="customEmailEnabled"
:initial-selected-template="selectedTemplate"
+ :initial-selected-file-template-project-id="selectedFileTemplateProjectId"
:initial-outgoing-name="outgoingName"
:initial-project-key="projectKey"
:templates="templates"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index fe2d376f1da..b8053bf9ab5 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -1,15 +1,8 @@
<script>
-import {
- GlButton,
- GlFormSelect,
- GlToggle,
- GlLoadingIcon,
- GlSprintf,
- GlFormInput,
- GlLink,
-} from '@gitlab/ui';
+import { GlButton, GlToggle, GlLoadingIcon, GlSprintf, GlFormInput, GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import ServiceDeskTemplateDropdown from './service_desk_template_dropdown.vue';
export default {
i18n: {
@@ -18,12 +11,12 @@ export default {
components: {
ClipboardButton,
GlButton,
- GlFormSelect,
GlToggle,
GlLoadingIcon,
GlSprintf,
GlFormInput,
GlLink,
+ ServiceDeskTemplateDropdown,
},
props: {
isEnabled: {
@@ -49,6 +42,11 @@ export default {
required: false,
default: '',
},
+ initialSelectedFileTemplateProjectId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
initialOutgoingName: {
type: String,
required: false,
@@ -73,14 +71,14 @@ export default {
data() {
return {
selectedTemplate: this.initialSelectedTemplate,
+ selectedFileTemplateProjectId: this.initialSelectedFileTemplateProjectId,
outgoingName: this.initialOutgoingName || __('GitLab Support Bot'),
projectKey: this.initialProjectKey,
+ searchTerm: '',
+ projectKeyError: null,
};
},
computed: {
- templateOptions() {
- return [''].concat(this.templates);
- },
hasProjectKeySupport() {
return Boolean(this.customEmailEnabled);
},
@@ -100,8 +98,21 @@ export default {
selectedTemplate: this.selectedTemplate,
outgoingName: this.outgoingName,
projectKey: this.projectKey,
+ fileTemplateProjectId: this.selectedFileTemplateProjectId,
});
},
+ templateChange({ selectedFileTemplateProjectId, selectedTemplate }) {
+ this.selectedFileTemplateProjectId = selectedFileTemplateProjectId;
+ this.selectedTemplate = selectedTemplate;
+ },
+ validateProjectKey() {
+ if (this.projectKey && !new RegExp(/^[a-z0-9_]+$/).test(this.projectKey)) {
+ this.projectKeyError = __('Only use lowercase letters, numbers, and underscores.');
+ return;
+ }
+
+ this.projectKeyError = null;
+ },
},
};
</script>
@@ -167,8 +178,17 @@ export default {
v-model.trim="projectKey"
data-testid="project-suffix"
class="form-control"
+ :state="!projectKeyError"
+ @blur="validateProjectKey"
/>
- <span v-if="hasProjectKeySupport" class="form-text text-muted">
+ <span v-if="hasProjectKeySupport && projectKeyError" class="form-text text-danger">
+ {{ projectKeyError }}
+ </span>
+ <span
+ v-if="hasProjectKeySupport"
+ class="form-text text-muted"
+ :class="{ 'gl-mt-2!': hasProjectKeySupport && projectKeyError }"
+ >
{{ __('A string appended to the project path to form the Service Desk email address.') }}
</span>
<span v-else class="form-text text-muted">
@@ -193,12 +213,13 @@ export default {
<label for="service-desk-template-select" class="mt-3">
{{ __('Template to append to all Service Desk issues') }}
</label>
- <gl-form-select
- id="service-desk-template-select"
- v-model="selectedTemplate"
- data-qa-selector="service_desk_template_dropdown"
- :options="templateOptions"
+ <service-desk-template-dropdown
+ :selected-template="selectedTemplate"
+ :selected-file-template-project-id="selectedFileTemplateProjectId"
+ :templates="templates"
+ @change="templateChange"
/>
+
<label for="service-desk-email-from-name" class="mt-3">
{{ __('Email display name') }}
</label>
@@ -210,6 +231,7 @@ export default {
<gl-button
variant="success"
class="gl-mt-5"
+ data-testid="save_service_desk_settings_button"
data-qa-selector="save_service_desk_settings_button"
:disabled="isTemplateSaving"
@click="onSaveTemplate"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
new file mode 100644
index 00000000000..bdd9f940d79
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
@@ -0,0 +1,115 @@
+<script>
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ },
+ props: {
+ selectedTemplate: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ templates: {
+ type: Array,
+ required: true,
+ },
+ selectedFileTemplateProjectId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ searchTerm: '',
+ };
+ },
+ computed: {
+ templateOptions() {
+ if (this.searchTerm) {
+ const filteredTemplates = [];
+ for (let i = 0; i < this.templates.length; i += 2) {
+ const sectionName = this.templates[i];
+ const availableTemplates = this.templates[i + 1];
+
+ const matchedTemplates = fuzzaldrinPlus.filter(availableTemplates, this.searchTerm, {
+ key: 'name',
+ });
+
+ if (matchedTemplates.length > 0) {
+ filteredTemplates.push(sectionName, matchedTemplates);
+ }
+ }
+
+ return filteredTemplates;
+ }
+
+ return this.templates;
+ },
+ },
+ methods: {
+ templateClick(template) {
+ // Clicking on the same template should unselect it
+ if (
+ template.name === this.selectedTemplate &&
+ template.project_id === this.selectedFileTemplateProjectId
+ ) {
+ this.$emit('change', {
+ selectedFileTemplateProjectId: null,
+ selectedTemplate: null,
+ });
+ return;
+ }
+
+ this.$emit('change', {
+ selectedFileTemplateProjectId: template.project_id,
+ selectedTemplate: template.key,
+ });
+ },
+ },
+ i18n: {
+ defaultDropdownText: __('Choose a template'),
+ },
+};
+</script>
+<template>
+ <gl-dropdown
+ id="service-desk-template-select"
+ :text="selectedTemplate || $options.i18n.defaultDropdownText"
+ :header-text="$options.i18n.defaultDropdownText"
+ data-qa-selector="service_desk_template_dropdown"
+ :block="true"
+ class="service-desk-template-select"
+ toggle-class="gl-m-0"
+ >
+ <template #header>
+ <gl-search-box-by-type v-model.trim="searchTerm" />
+ </template>
+ <template v-for="item in templateOptions">
+ <gl-dropdown-section-header v-if="!Array.isArray(item)" :key="item">
+ {{ item }}
+ </gl-dropdown-section-header>
+ <template v-else>
+ <gl-dropdown-item
+ v-for="template in item"
+ :key="template.key"
+ :is-check-item="true"
+ :is-checked="
+ template.project_id === selectedFileTemplateProjectId &&
+ template.name === selectedTemplate
+ "
+ @click="() => templateClick(template)"
+ >
+ {{ template.name }}
+ </gl-dropdown-item>
+ </template>
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
index f842ffaaa2b..e14cdee17ce 100644
--- a/app/assets/javascripts/projects/settings_service_desk/index.js
+++ b/app/assets/javascripts/projects/settings_service_desk/index.js
@@ -18,6 +18,7 @@ export default () => {
outgoingName,
projectKey,
selectedTemplate,
+ selectedFileTemplateProjectId,
templates,
} = el.dataset;
@@ -32,6 +33,7 @@ export default () => {
outgoingName,
projectKey,
selectedTemplate,
+ selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null,
templates: JSON.parse(templates),
},
render: (createElement) => createElement(ServiceDeskRoot),
diff --git a/app/assets/javascripts/projects/storage_counter/components/storage_table.vue b/app/assets/javascripts/projects/storage_counter/components/storage_table.vue
index 7047fd925fb..a42a9711572 100644
--- a/app/assets/javascripts/projects/storage_counter/components/storage_table.vue
+++ b/app/assets/javascripts/projects/storage_counter/components/storage_table.vue
@@ -1,9 +1,10 @@
<script>
-import { GlLink, GlIcon, GlTable, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlIcon, GlTableLite as GlTable, GlSprintf } from '@gitlab/ui';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { thWidthClass } from '~/lib/utils/table_utility';
import { sprintf } from '~/locale';
import { PROJECT_TABLE_LABELS, HELP_LINK_ARIA_LABEL } from '../constants';
+import StorageTypeIcon from './storage_type_icon.vue';
export default {
name: 'StorageTable',
@@ -12,6 +13,7 @@ export default {
GlIcon,
GlTable,
GlSprintf,
+ StorageTypeIcon,
},
props: {
storageTypes: {
@@ -48,31 +50,39 @@ export default {
<template>
<gl-table :items="storageTypes" :fields="$options.projectTableFields">
<template #cell(storageType)="{ item }">
- <p class="gl-font-weight-bold gl-mb-0" :data-testid="`${item.storageType.id}-name`">
- {{ item.storageType.name }}
- <gl-link
- v-if="item.storageType.helpPath"
- :href="item.storageType.helpPath"
- target="_blank"
- :aria-label="helpLinkAriaLabel(item.storageType.name)"
- :data-testid="`${item.storageType.id}-help-link`"
- >
- <gl-icon name="question" :size="12" />
- </gl-link>
- </p>
- <p class="gl-mb-0" :data-testid="`${item.storageType.id}-description`">
- {{ item.storageType.description }}
- </p>
- <p v-if="item.storageType.warningMessage" class="gl-mb-0 gl-font-sm">
- <gl-icon name="warning" :size="12" />
- <gl-sprintf :message="item.storageType.warningMessage">
- <template #warningLink="{ content }">
- <gl-link :href="item.storageType.warningLink" target="_blank" class="gl-font-sm">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
+ <div class="gl-display-flex gl-flex-direction-row">
+ <storage-type-icon
+ :name="item.storageType.id"
+ :data-testid="`${item.storageType.id}-icon`"
+ />
+ <div>
+ <p class="gl-font-weight-bold gl-mb-0" :data-testid="`${item.storageType.id}-name`">
+ {{ item.storageType.name }}
+ <gl-link
+ v-if="item.storageType.helpPath"
+ :href="item.storageType.helpPath"
+ target="_blank"
+ :aria-label="helpLinkAriaLabel(item.storageType.name)"
+ :data-testid="`${item.storageType.id}-help-link`"
+ >
+ <gl-icon name="question" :size="12" />
+ </gl-link>
+ </p>
+ <p class="gl-mb-0" :data-testid="`${item.storageType.id}-description`">
+ {{ item.storageType.description }}
+ </p>
+ <p v-if="item.storageType.warningMessage" class="gl-mb-0 gl-font-sm">
+ <gl-icon name="warning" :size="12" />
+ <gl-sprintf :message="item.storageType.warningMessage">
+ <template #warningLink="{ content }">
+ <gl-link :href="item.storageType.warningLink" target="_blank" class="gl-font-sm">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+ </div>
</template>
</gl-table>
</template>
diff --git a/app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue b/app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue
new file mode 100644
index 00000000000..bc7cd42df1e
--- /dev/null
+++ b/app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ components: { GlIcon },
+ props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ methods: {
+ iconName(storageTypeName) {
+ const defaultStorageTypeIcon = 'disk';
+ const storageTypeIconMap = {
+ lfsObjectsSize: 'doc-image',
+ snippetsSize: 'snippet',
+ uploadsSize: 'upload',
+ repositorySize: 'infrastructure-registry',
+ packagesSize: 'package',
+ };
+
+ return storageTypeIconMap[`${storageTypeName}`] ?? defaultStorageTypeIcon;
+ },
+ },
+};
+</script>
+<template>
+ <span
+ class="gl-display-inline-flex gl-align-items-flex-start gl-justify-content-center gl-min-w-8 gl-pr-2 gl-pt-1"
+ >
+ <gl-icon :name="iconName(name)" :size="16" class="gl-mt-1" />
+ </span>
+</template>
diff --git a/app/assets/javascripts/projects/storage_counter/constants.js b/app/assets/javascripts/projects/storage_counter/constants.js
index d9b28abfbe7..df4b1800dff 100644
--- a/app/assets/javascripts/projects/storage_counter/constants.js
+++ b/app/assets/javascripts/projects/storage_counter/constants.js
@@ -6,13 +6,13 @@ export const PROJECT_STORAGE_TYPES = [
name: s__('UsageQuota|Artifacts'),
description: s__('UsageQuota|Pipeline artifacts and job artifacts, created with CI/CD.'),
warningMessage: s__(
- 'UsageQuota|There is a known issue with Artifact storage where the total could be incorrect for some projects. More details and progress are available in %{warningLinkStart}the epic%{warningLinkEnd}.',
+ 'UsageQuota|Because of a known issue, the artifact total for some projects may be incorrect. For more details, read %{warningLinkStart}the epic%{warningLinkEnd}.',
),
warningLink: 'https://gitlab.com/groups/gitlab-org/-/epics/5380',
},
{
id: 'lfsObjectsSize',
- name: s__('UsageQuota|LFS Storage'),
+ name: s__('UsageQuota|LFS storage'),
description: s__('UsageQuota|Audio samples, videos, datasets, and graphics.'),
},
{
@@ -23,7 +23,7 @@ export const PROJECT_STORAGE_TYPES = [
{
id: 'repositorySize',
name: s__('UsageQuota|Repository'),
- description: s__('UsageQuota|Git repository, managed by the Gitaly service.'),
+ description: s__('UsageQuota|Git repository.'),
},
{
id: 'snippetsSize',
@@ -51,11 +51,11 @@ export const ERROR_MESSAGE = s__(
'UsageQuota|Something went wrong while fetching project storage statistics',
);
-export const LEARN_MORE_LABEL = s__('Learn more.');
+export const LEARN_MORE_LABEL = __('Learn more.');
export const USAGE_QUOTAS_LABEL = s__('UsageQuota|Usage Quotas');
export const HELP_LINK_ARIA_LABEL = s__('UsageQuota|%{linkTitle} help link');
export const TOTAL_USAGE_DEFAULT_TEXT = __('N/A');
-export const TOTAL_USAGE_TITLE = s__('UsageQuota|Usage Breakdown');
+export const TOTAL_USAGE_TITLE = s__('UsageQuota|Usage breakdown');
export const TOTAL_USAGE_SUBTITLE = s__(
- 'UsageQuota|Includes project registry, artifacts, packages, wiki, uploads and other items.',
+ 'UsageQuota|Includes artifacts, repositories, wiki, uploads, and other items.',
);
diff --git a/app/assets/javascripts/projects/storage_counter/index.js b/app/assets/javascripts/projects/storage_counter/index.js
index 10668f08402..15796bc1870 100644
--- a/app/assets/javascripts/projects/storage_counter/index.js
+++ b/app/assets/javascripts/projects/storage_counter/index.js
@@ -25,7 +25,7 @@ export default (containerId = 'js-project-storage-count-app') => {
} = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/projects/storage_counter/utils.js b/app/assets/javascripts/projects/storage_counter/utils.js
index cb26603fff5..9fca9d88f46 100644
--- a/app/assets/javascripts/projects/storage_counter/utils.js
+++ b/app/assets/javascripts/projects/storage_counter/utils.js
@@ -14,10 +14,6 @@ export const parseGetProjectStorageResults = (data, helpLinks) => {
}
const { storageSize, ...storageStatistics } = projectStatistics;
const storageTypes = PROJECT_STORAGE_TYPES.reduce((types, currentType) => {
- if (!storageStatistics[currentType.id]) {
- return types;
- }
-
const helpPathKey = currentType.id.replace(`Size`, `HelpPagePath`);
const helpPath = helpLinks[helpPathKey];
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index f6f409873c8..a79da00de43 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -58,7 +58,7 @@ export default {
};
this.isLoading = false;
createFlash({
- message: s__('Something went wrong on our end'),
+ message: __('Something went wrong on our end'),
});
},
initPolling() {
diff --git a/app/assets/javascripts/projects/upload_file.js b/app/assets/javascripts/projects/upload_file.js
new file mode 100644
index 00000000000..597965eabfc
--- /dev/null
+++ b/app/assets/javascripts/projects/upload_file.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import createRouter from '~/repository/router';
+import UploadButton from './details/upload_button.vue';
+
+export const initUploadFileTrigger = () => {
+ const uploadFileTriggerEl = document.querySelector('.js-upload-file-trigger');
+
+ if (!uploadFileTriggerEl) return false;
+
+ const {
+ targetBranch,
+ originalBranch,
+ canPushCode,
+ path,
+ projectPath,
+ } = uploadFileTriggerEl.dataset;
+
+ return new Vue({
+ el: uploadFileTriggerEl,
+ router: createRouter(projectPath, originalBranch),
+ provide: {
+ targetBranch,
+ originalBranch,
+ canPushCode: parseBoolean(canPushCode),
+ path,
+ projectPath,
+ },
+ render(h) {
+ return h(UploadButton);
+ },
+ });
+};
diff --git a/app/assets/javascripts/projects/upload_file_experiment.js b/app/assets/javascripts/projects/upload_file_experiment.js
deleted file mode 100644
index a7519f2bce8..00000000000
--- a/app/assets/javascripts/projects/upload_file_experiment.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import createRouter from '~/repository/router';
-import UploadButton from './details/upload_button.vue';
-
-export const initUploadFileTrigger = () => {
- const uploadFileTriggerEl = document.querySelector('.js-upload-file-experiment-trigger');
-
- if (!uploadFileTriggerEl) return false;
-
- const {
- targetBranch,
- originalBranch,
- canPushCode,
- path,
- projectPath,
- } = uploadFileTriggerEl.dataset;
-
- return new Vue({
- el: uploadFileTriggerEl,
- router: createRouter(projectPath, originalBranch),
- provide: {
- targetBranch,
- originalBranch,
- canPushCode: parseBoolean(canPushCode),
- path,
- projectPath,
- },
- render(h) {
- return h(UploadButton);
- },
- });
-};
diff --git a/app/assets/javascripts/projects/upload_file_experiment_tracking.js b/app/assets/javascripts/projects/upload_file_experiment_tracking.js
deleted file mode 100644
index c5e93f19b32..00000000000
--- a/app/assets/javascripts/projects/upload_file_experiment_tracking.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import ExperimentTracking from '~/experimentation/experiment_tracking';
-
-export const trackFileUploadEvent = (eventName) => {
- const isEmpty = Boolean(document.querySelector('.project-home-panel.empty-project'));
- const property = isEmpty ? 'empty' : 'nonempty';
- const label = 'blob-upload-modal';
- const FileUploadTracking = new ExperimentTracking('empty_repo_upload', { label, property });
- FileUploadTracking.event(eventName);
-};
diff --git a/app/assets/javascripts/ref/constants.js b/app/assets/javascripts/ref/constants.js
index 1cef986a83d..397e3ed2ac8 100644
--- a/app/assets/javascripts/ref/constants.js
+++ b/app/assets/javascripts/ref/constants.js
@@ -15,9 +15,9 @@ export const DEFAULT_I18N = Object.freeze({
searchPlaceholder: __('Search by Git revision'),
noResultsWithQuery: __('No matching results for "%{query}"'),
noResults: __('No matching results'),
- branchesErrorMessage: __('An error occurred while fetching branches. Retry the search.'),
+ branchesErrorMessage: __('An error occurred while fetching branches. Retry the search.'),
tagsErrorMessage: __('An error occurred while fetching tags. Retry the search.'),
- commitsErrorMessage: __('An error occurred while fetching commits. Retry the search.'),
+ commitsErrorMessage: __('An error occurred while fetching commits. Retry the search.'),
branches: __('Branches'),
tags: __('Tags'),
commits: __('Commits'),
diff --git a/app/assets/javascripts/registry/explorer/graphql/index.js b/app/assets/javascripts/registry/explorer/graphql/index.js
deleted file mode 100644
index d934bcc7419..00000000000
--- a/app/assets/javascripts/registry/explorer/graphql/index.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-
-Vue.use(VueApollo);
-
-export const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- batchMax: 1,
- assumeImmutableResults: true,
- },
- ),
-});
diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
index 05858c7469d..50835142d28 100644
--- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
+++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
-import { sprintf, n__, s__ } from '~/locale';
+import { sprintf, __, n__ } from '~/locale';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import { parseIssuableData } from '../../issue_show/utils/parse_data';
@@ -40,7 +40,7 @@ export default {
this.totalCount,
);
- return sprintf(s__('%{mrText}, this issue will be closed automatically.'), { mrText });
+ return sprintf(__('%{mrText}, this issue will be closed automatically.'), { mrText });
},
},
mounted() {
@@ -64,58 +64,51 @@ export default {
</script>
<template>
- <div
- v-if="isFetchingMergeRequests || (!isFetchingMergeRequests && totalCount)"
- id="related-merge-requests"
- >
- <div id="merge-requests" class="card card-slim mt-3">
+ <div v-if="isFetchingMergeRequests || (!isFetchingMergeRequests && totalCount)">
+ <div class="card card-slim gl-mt-5">
<div class="card-header">
- <div class="card-title mt-0 mb-0 h5 merge-requests-title position-relative">
+ <div
+ class="card-title gl-relative gl-display-flex gl-align-items-center gl-line-height-20 gl-font-weight-bold gl-m-0"
+ >
<gl-link
- id="user-content-related-merge-requests"
- class="anchor position-absolute text-decoration-none"
+ class="anchor gl-absolute gl-text-decoration-none"
href="#related-merge-requests"
- aria-hidden="true"
+ aria-labelledby="related-merge-requests"
/>
- <span class="mr-1">
+ <h3 id="related-merge-requests" class="gl-font-base gl-m-0">
{{ __('Related merge requests') }}
- </span>
- <div v-if="totalCount" class="d-inline-flex lh-100 align-middle">
- <div
- class="mr-count-badge gl-display-inline-flex gl-align-items-center gl-py-2 gl-px-3"
- >
- <svg class="s16 mr-1 text-secondary">
- <gl-icon name="merge-request" class="mr-1 text-secondary" />
- </svg>
- <span class="js-items-count">{{ totalCount }}</span>
- </div>
- </div>
+ </h3>
+ <template v-if="totalCount">
+ <gl-icon name="merge-request" class="gl-ml-5 gl-mr-2 gl-text-gray-500" />
+ <span data-testid="count">{{ totalCount }}</span>
+ </template>
</div>
</div>
- <div>
- <div v-if="isFetchingMergeRequests" class="qa-related-merge-requests-loading-icon">
- <gl-loading-icon size="sm" label="Fetching related merge requests" class="py-2" />
- </div>
- <ul v-else class="content-list related-items-list">
- <li v-for="mr in mergeRequests" :key="mr.id" class="list-item pt-0 pb-0">
- <related-issuable-item
- :id-key="mr.id"
- :display-reference="mr.reference"
- :title="mr.title"
- :milestone="mr.milestone"
- :assignees="getAssignees(mr)"
- :created-at="mr.created_at"
- :closed-at="mr.closed_at"
- :merged-at="mr.merged_at"
- :path="mr.web_url"
- :state="mr.state"
- :is-merge-request="true"
- :pipeline-status="mr.head_pipeline && mr.head_pipeline.detailed_status"
- path-id-separator="!"
- />
- </li>
- </ul>
- </div>
+ <gl-loading-icon
+ v-if="isFetchingMergeRequests"
+ size="sm"
+ label="Fetching related merge requests"
+ class="gl-py-3"
+ />
+ <ul v-else class="content-list related-items-list">
+ <li v-for="mr in mergeRequests" :key="mr.id" class="list-item gl-m-0! gl-p-0!">
+ <related-issuable-item
+ :id-key="mr.id"
+ :display-reference="mr.reference"
+ :title="mr.title"
+ :milestone="mr.milestone"
+ :assignees="getAssignees(mr)"
+ :created-at="mr.created_at"
+ :closed-at="mr.closed_at"
+ :merged-at="mr.merged_at"
+ :path="mr.web_url"
+ :state="mr.state"
+ :is-merge-request="true"
+ :pipeline-status="mr.head_pipeline && mr.head_pipeline.detailed_status"
+ path-id-separator="!"
+ />
+ </li>
+ </ul>
</div>
<div
v-if="hasClosingMergeRequest && !isFetchingMergeRequests"
diff --git a/app/assets/javascripts/related_merge_requests/store/actions.js b/app/assets/javascripts/related_merge_requests/store/actions.js
index 652d03a0fd0..94abb50de89 100644
--- a/app/assets/javascripts/related_merge_requests/store/actions.js
+++ b/app/assets/javascripts/related_merge_requests/store/actions.js
@@ -1,7 +1,7 @@
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils';
-import { s__ } from '~/locale';
+import { __ } from '~/locale';
import * as types from './mutation_types';
const REQUEST_PAGE_COUNT = 100;
@@ -30,7 +30,7 @@ export const fetchMergeRequests = ({ state, dispatch }) => {
.catch(() => {
dispatch('receiveDataError');
createFlash({
- message: s__('Something went wrong while fetching related merge requests.'),
+ message: __('Something went wrong while fetching related merge requests.'),
});
});
};
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index 80f59485426..9e05d00a98d 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -95,6 +95,8 @@ export default {
noRefSelected: __('No tag selected'),
dropdownHeader: __('Tag name'),
searchPlaceholder: __('Search or create tag'),
+ label: __('Tag name'),
+ labelDescription: __('*Required'),
},
createFrom: {
noRefSelected: __('No source selected'),
@@ -108,11 +110,12 @@ export default {
<template>
<div>
<gl-form-group
- :label="__('Tag name')"
- :label-for="tagNameInputId"
data-testid="tag-name-field"
:state="!showTagNameValidationError"
:invalid-feedback="__('Tag name is required')"
+ :label="$options.translations.tagName.label"
+ :label-for="tagNameInputId"
+ :label-description="$options.translations.tagName.labelDescription"
>
<form-field-container>
<ref-selector
diff --git a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
index f2d89dbe682..c69481150e0 100644
--- a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
+++ b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
@@ -6,7 +6,7 @@
# When the `releases_index_apollo_client` feature flag is
# removed, this query should be removed entirely.
-query allReleases(
+query allReleasesDeprecated(
$fullPath: ID!
$first: Int
$last: Int
diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js
index 59f6ebfc928..86fa72d1496 100644
--- a/app/assets/javascripts/releases/mount_index.js
+++ b/app/assets/javascripts/releases/mount_index.js
@@ -24,7 +24,6 @@ export default () => {
// the purpose of making separate requests. So we explicitly
// disable batching on this page.
batchMax: 1,
- assumeImmutableResults: true,
},
),
});
diff --git a/app/assets/javascripts/releases/mount_show.js b/app/assets/javascripts/releases/mount_show.js
index 686f9e294b7..7272880197a 100644
--- a/app/assets/javascripts/releases/mount_show.js
+++ b/app/assets/javascripts/releases/mount_show.js
@@ -6,12 +6,7 @@ import ReleaseShowApp from './components/app_show.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
export default () => {
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 7ad9fb56972..2cc5a8a79d2 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
@@ -17,12 +17,16 @@ import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer, viewerProps } from './blob_viewers';
export default {
+ i18n: {
+ pipelineEditor: __('Pipeline Editor'),
+ },
components: {
BlobHeader,
BlobEdit,
BlobButtonGroup,
BlobContent,
GlLoadingIcon,
+ GlButton,
ForkSuggestion,
},
mixins: [getRefMixin],
@@ -105,6 +109,7 @@ export default {
rawPath: '',
externalStorageUrl: '',
replacePath: '',
+ pipelineEditorPath: '',
deletePath: '',
simpleViewer: {},
richViewer: null,
@@ -242,6 +247,18 @@ export default {
:needs-to-fork="showForkSuggestion"
@edit="editBlob"
/>
+
+ <gl-button
+ v-if="blobInfo.pipelineEditorPath"
+ class="gl-mr-3"
+ category="secondary"
+ variant="confirm"
+ data-testid="pipeline-editor"
+ :href="blobInfo.pipelineEditorPath"
+ >
+ {{ $options.i18n.pipelineEditor }}
+ </gl-button>
+
<blob-button-group
v-if="isLoggedIn"
:path="path"
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
index a307b7c0b8a..4a8cedb60b4 100644
--- a/app/assets/javascripts/repository/components/delete_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -116,15 +116,14 @@ export default {
],
};
},
- /* eslint-disable dot-notation */
showCreateNewMrToggle() {
- return this.canPushCode && this.form.fields['branch_name'].value !== this.originalBranch;
+ return this.canPushCode && this.form.fields.branch_name.value !== this.originalBranch;
},
formCompleted() {
- return this.form.fields['commit_message'].value && this.form.fields['branch_name'].value;
+ return this.form.fields.commit_message.value && this.form.fields.branch_name.value;
},
showHint() {
- const splitCommitMessageByLineBreak = this.form.fields['commit_message'].value
+ const splitCommitMessageByLineBreak = this.form.fields.commit_message.value
.trim()
.split('\n');
const [firstLine, ...otherLines] = splitCommitMessageByLineBreak;
@@ -136,7 +135,7 @@ export default {
otherLines.some((text) => text.length > COMMIT_MESSAGE_BODY_MAX_LENGTH);
return (
- !this.form.fields['commit_message'].feedback &&
+ !this.form.fields.commit_message.feedback &&
(hasFirstLineExceedMaxLength || hasOtherLineExceedMaxLength)
);
},
@@ -173,9 +172,7 @@ export default {
<input type="hidden" name="_method" value="delete" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<template v-if="emptyRepo">
- <!-- Once "empty_repo_upload_experiment" is made available, will need to add class 'js-branch-name'
- Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335721 -->
- <input type="hidden" name="branch_name" :value="originalBranch" />
+ <input type="hidden" name="branch_name" :value="originalBranch" class="js-branch-name" />
</template>
<template v-else>
<input type="hidden" name="original_branch" :value="originalBranch" />
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 5c713796bd6..62066973ee6 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -1,5 +1,12 @@
<script>
-import { GlTooltipDirective, GlLink, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlTooltipDirective,
+ GlLink,
+ GlButton,
+ GlButtonGroup,
+ GlLoadingIcon,
+ GlSafeHtmlDirective,
+} from '@gitlab/ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { sprintf, s__ } from '~/locale';
@@ -23,6 +30,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
mixins: [getRefMixin],
apollo: {
@@ -96,6 +104,9 @@ export default {
},
},
defaultAvatarUrl,
+ safeHtmlConfig: {
+ ADD_TAGS: ['gl-emoji'],
+ },
};
</script>
@@ -121,10 +132,10 @@ export default {
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<gl-link
+ v-safe-html:[$options.safeHtmlConfig]="commit.titleHtml"
:href="commit.webPath"
:class="{ 'font-italic': !commit.message }"
class="commit-row-message item-title"
- v-html="commit.titleHtml /* eslint-disable-line vue/no-v-html */"
/>
<gl-button
v-if="commit.descriptionHtml"
@@ -150,15 +161,15 @@ export default {
</div>
<pre
v-if="commitDescription"
+ v-safe-html:[$options.safeHtmlConfig]="commitDescription"
:class="{ 'd-block': showDescription }"
class="commit-row-description gl-mb-3"
- v-html="commitDescription /* eslint-disable-line vue/no-v-html */"
></pre>
</div>
<div class="commit-actions flex-row">
<div
v-if="commit.signatureHtml"
- v-html="commit.signatureHtml /* eslint-disable-line vue/no-v-html */"
+ v-safe-html:[$options.safeHtmlConfig]="commit.signatureHtml"
></div>
<div v-if="commit.pipeline" class="ci-status-link">
<gl-link
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 5010d60f374..bd06c064ab7 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -11,7 +11,6 @@ import {
GlIntersectionObserver,
} from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
-import filesQuery from 'shared_queries/repository/files.query.graphql';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import { TREE_PAGE_SIZE } from '~/repository/constants';
@@ -178,8 +177,7 @@ export default {
return this.isFolder ? this.loadFolder() : this.loadBlob();
},
loadFolder() {
- const query = this.glFeatures.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery;
- this.apolloQuery(query, {
+ this.apolloQuery(paginatedTreeQuery, {
projectPath: this.projectPath,
ref: this.ref,
path: this.path,
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index 16dfe3cfb14..ffe8d5531f8 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -1,5 +1,4 @@
<script>
-import filesQuery from 'shared_queries/repository/files.query.graphql';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import createFlash from '~/flash';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -72,9 +71,6 @@ export default {
hasShowMore() {
return !this.clickedShowMore && this.pageLimitReached;
},
- paginatedTreeEnabled() {
- return this.glFeatures.paginatedTreeGraphqlQuery;
- },
},
watch: {
@@ -101,7 +97,7 @@ export default {
return this.$apollo
.query({
- query: this.paginatedTreeEnabled ? paginatedTreeQuery : filesQuery,
+ query: paginatedTreeQuery,
variables: {
projectPath: this.projectPath,
ref: this.ref,
@@ -114,20 +110,19 @@ export default {
if (data.errors) throw data.errors;
if (!data?.project?.repository || originalPath !== (this.path || '/')) return;
- const pageInfo = this.paginatedTreeEnabled
- ? data.project.repository.paginatedTree.pageInfo
- : this.hasNextPage(data.project.repository.tree);
+ const {
+ project: {
+ repository: {
+ paginatedTree: { pageInfo },
+ },
+ },
+ } = data;
this.isLoadingFiles = false;
this.entries = Object.keys(this.entries).reduce(
(acc, key) => ({
...acc,
- [key]: this.normalizeData(
- key,
- this.paginatedTreeEnabled
- ? data.project.repository.paginatedTree.nodes[0][key]
- : data.project.repository.tree[key].edges,
- ),
+ [key]: this.normalizeData(key, data.project.repository.paginatedTree.nodes[0][key]),
}),
{},
);
@@ -149,9 +144,7 @@ export default {
});
},
normalizeData(key, data) {
- return this.entries[key].concat(
- this.paginatedTreeEnabled ? data.nodes : data.map(({ node }) => node),
- );
+ return this.entries[key].concat(data.nodes);
},
hasNextPage(data) {
return []
diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue
index 0199b893453..11e5b5608cb 100644
--- a/app/assets/javascripts/repository/components/upload_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue
@@ -15,7 +15,6 @@ import { ContentTypeMultipartFormData } from '~/lib/utils/headers';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import {
SECONDARY_OPTIONS_TEXT,
@@ -165,9 +164,6 @@ export default {
},
})
.then((response) => {
- if (!this.replacePath) {
- trackFileUploadEvent('click_upload_modal_form_submit');
- }
visitUrl(response.data.filePath);
})
.catch(() => {
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index 4892e54ebef..96d712ce9b4 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -64,7 +64,6 @@ const defaultClient = createDefaultClient(
/* eslint-enable @gitlab/require-i18n-strings */
},
},
- assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/repository/mixins/preload.js b/app/assets/javascripts/repository/mixins/preload.js
index a2ddcbf0e4c..30c36dee48f 100644
--- a/app/assets/javascripts/repository/mixins/preload.js
+++ b/app/assets/javascripts/repository/mixins/preload.js
@@ -1,4 +1,3 @@
-import filesQuery from 'shared_queries/repository/files.query.graphql';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import projectPathQuery from '../queries/project_path.query.graphql';
import getRefMixin from './get_ref';
@@ -22,7 +21,7 @@ export default {
return this.$apollo
.query({
- query: gon.features.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery,
+ query: paginatedTreeQuery,
variables: {
projectPath: this.projectPath,
ref: this.ref,
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index 8e0b5e21ca3..cf3892802fd 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -31,6 +31,7 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
storedExternally
rawPath
replacePath
+ pipelineEditorPath
simpleViewer {
fileType
tooLarge
diff --git a/app/assets/javascripts/rest_api.js b/app/assets/javascripts/rest_api.js
index 29642b6633f..48a15954035 100644
--- a/app/assets/javascripts/rest_api.js
+++ b/app/assets/javascripts/rest_api.js
@@ -3,6 +3,7 @@ export * from './api/projects_api';
export * from './api/user_api';
export * from './api/markdown_api';
export * from './api/bulk_imports_api';
+export * from './api/namespaces_api';
// Note: It's not possible to spy on methods imported from this file in
// Jest tests.
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 381421cdc23..3c8533dd06d 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -3,6 +3,7 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
import { hide, fixTitle } from '~/tooltips';
+import { DEBOUNCE_DROPDOWN_DELAY } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { sprintf, s__, __ } from './locale';
@@ -130,8 +131,10 @@ Sidebar.prototype.openDropdown = function (blockOrName) {
// Wait for the sidebar to trigger('click') open
// so it doesn't cause our dropdown to close preemptively
setTimeout(() => {
- $block.find('.js-sidebar-dropdown-toggle').trigger('click');
- });
+ if (!gon.features?.labelsWidget && !$block.hasClass('labels-select-wrapper')) {
+ $block.find('.js-sidebar-dropdown-toggle').trigger('click');
+ }
+ }, DEBOUNCE_DROPDOWN_DELAY);
};
Sidebar.prototype.setCollapseAfterUpdate = function ($block) {
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index c8513a0b803..3edb658eaf5 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -1,18 +1,26 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlBadge, GlLink } from '@gitlab/ui';
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
-import { formatNumber, sprintf, __ } from '~/locale';
+import { sprintf, __ } from '~/locale';
+
+import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
-import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerName from '../components/runner_name.vue';
import RunnerPagination from '../components/runner_pagination.vue';
+import RunnerTypeTabs from '../components/runner_type_tabs.vue';
+
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
-import { typeTokenConfig } from '../components/search_tokens/type_token_config';
-import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
+import {
+ ADMIN_FILTERED_SEARCH_NAMESPACE,
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ I18N_FETCH_ERROR,
+} from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
import {
fromUrlQueryToSearch,
@@ -24,19 +32,37 @@ import { captureException } from '../sentry_utils';
export default {
name: 'AdminRunnersApp',
components: {
+ GlBadge,
GlLink,
+ RegistrationDropdown,
RunnerFilteredSearchBar,
RunnerList,
- RunnerManualSetupHelp,
RunnerName,
RunnerPagination,
+ RunnerTypeTabs,
},
props: {
+ registrationToken: {
+ type: String,
+ required: true,
+ },
activeRunnersCount: {
- type: Number,
+ type: String,
required: true,
},
- registrationToken: {
+ allRunnersCount: {
+ type: String,
+ required: true,
+ },
+ instanceRunnersCount: {
+ type: String,
+ required: true,
+ },
+ groupRunnersCount: {
+ type: String,
+ required: true,
+ },
+ projectRunnersCount: {
type: String,
required: true,
},
@@ -86,13 +112,12 @@ export default {
},
activeRunnersMessage() {
return sprintf(__('Runners currently online: %{active_runners_count}'), {
- active_runners_count: formatNumber(this.activeRunnersCount),
+ active_runners_count: this.activeRunnersCount,
});
},
searchTokens() {
return [
statusTokenConfig,
- typeTokenConfig,
{
...tagTokenConfig,
recentTokenValuesStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
@@ -116,6 +141,20 @@ export default {
this.reportToSentry(error);
},
methods: {
+ tabCount({ runnerType }) {
+ switch (runnerType) {
+ case null:
+ return this.allRunnersCount;
+ case INSTANCE_TYPE:
+ return this.instanceRunnersCount;
+ case GROUP_TYPE:
+ return this.groupRunnersCount;
+ case PROJECT_TYPE:
+ return this.projectRunnersCount;
+ default:
+ return null;
+ }
+ },
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
@@ -126,10 +165,30 @@ export default {
</script>
<template>
<div>
- <runner-manual-setup-help
- :registration-token="registrationToken"
- :type="$options.INSTANCE_TYPE"
- />
+ <div
+ class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
+ >
+ <runner-type-tabs
+ v-model="search"
+ class="gl-w-full"
+ content-class="gl-display-none"
+ nav-class="gl-border-none!"
+ >
+ <template #title="{ tab }">
+ {{ tab.title }}
+ <gl-badge v-if="tabCount(tab)" class="gl-ml-1" size="sm">
+ {{ tabCount(tab) }}
+ </gl-badge>
+ </template>
+ </runner-type-tabs>
+
+ <registration-dropdown
+ class="gl-w-full gl-sm-w-auto gl-mr-auto"
+ :registration-token="registrationToken"
+ :type="$options.INSTANCE_TYPE"
+ right
+ />
+ </div>
<runner-filtered-search-bar
v-model="search"
diff --git a/app/assets/javascripts/runner/admin_runners/index.js b/app/assets/javascripts/runner/admin_runners/index.js
index 1eec1019b73..62da6cbfa2b 100644
--- a/app/assets/javascripts/runner/admin_runners/index.js
+++ b/app/assets/javascripts/runner/admin_runners/index.js
@@ -1,8 +1,10 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import AdminRunnersApp from './admin_runners_app.vue';
+Vue.use(GlToast);
Vue.use(VueApollo);
export const initAdminRunners = (selector = '#js-admin-runners') => {
@@ -14,15 +16,19 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
// TODO `activeRunnersCount` should be implemented using a GraphQL API
// https://gitlab.com/gitlab-org/gitlab/-/issues/333806
- const { activeRunnersCount, registrationToken, runnerInstallHelpPage } = el.dataset;
+ const {
+ runnerInstallHelpPage,
+ registrationToken,
+
+ activeRunnersCount,
+ allRunnersCount,
+ instanceRunnersCount,
+ groupRunnersCount,
+ projectRunnersCount,
+ } = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
return new Vue({
@@ -34,8 +40,15 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
render(h) {
return h(AdminRunnersApp, {
props: {
- activeRunnersCount: parseInt(activeRunnersCount, 10),
registrationToken,
+
+ // All runner counts are returned as formatted
+ // strings, we do not use `parseInt`.
+ activeRunnersCount,
+ allRunnersCount,
+ instanceRunnersCount,
+ groupRunnersCount,
+ projectRunnersCount,
},
});
},
diff --git a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
index e26bdbf1aea..c4bddb7b398 100644
--- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
@@ -3,7 +3,7 @@ import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
import { __, s__ } from '~/locale';
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
-import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
+import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
const i18n = {
@@ -71,7 +71,7 @@ export default {
runnerUpdate: { errors },
},
} = await this.$apollo.mutate({
- mutation: runnerUpdateMutation,
+ mutation: runnerActionsUpdateMutation,
variables: {
input: {
id: this.runner.id,
diff --git a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
new file mode 100644
index 00000000000..9ba1192bc8c
--- /dev/null
+++ b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
@@ -0,0 +1,40 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+
+import RunnerContactedStateBadge from '../runner_contacted_state_badge.vue';
+import RunnerPausedBadge from '../runner_paused_badge.vue';
+
+import { I18N_LOCKED_RUNNER_DESCRIPTION, I18N_PAUSED_RUNNER_DESCRIPTION } from '../../constants';
+
+export default {
+ components: {
+ RunnerContactedStateBadge,
+ RunnerPausedBadge,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ paused() {
+ return !this.runner.active;
+ },
+ },
+ i18n: {
+ I18N_LOCKED_RUNNER_DESCRIPTION,
+ I18N_PAUSED_RUNNER_DESCRIPTION,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <runner-contacted-state-badge :runner="runner" size="sm" />
+ <runner-paused-badge v-if="paused" size="sm" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue
index 886b5cb29fc..3b476997915 100644
--- a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue
@@ -1,11 +1,21 @@
<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import RunnerName from '../runner_name.vue';
+import RunnerTypeBadge from '../runner_type_badge.vue';
+
+import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../../constants';
export default {
components: {
+ GlIcon,
TooltipOnTruncate,
RunnerName,
+ RunnerTypeBadge,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
props: {
runner: {
@@ -14,10 +24,19 @@ export default {
},
},
computed: {
+ runnerType() {
+ return this.runner.runnerType;
+ },
+ locked() {
+ return this.runner.locked;
+ },
description() {
return this.runner.description;
},
},
+ i18n: {
+ I18N_LOCKED_RUNNER_DESCRIPTION,
+ },
};
</script>
@@ -26,6 +45,14 @@ export default {
<slot :runner="runner" name="runner-name">
<runner-name :runner="runner" />
</slot>
+
+ <runner-type-badge :type="runnerType" size="sm" />
+ <gl-icon
+ v-if="locked"
+ v-gl-tooltip
+ :title="$options.i18n.I18N_LOCKED_RUNNER_DESCRIPTION"
+ name="lock"
+ />
<tooltip-on-truncate class="gl-display-block" :title="description" truncate-target="child">
<div class="gl-text-truncate">
{{ description }}
diff --git a/app/assets/javascripts/runner/components/cells/runner_type_cell.vue b/app/assets/javascripts/runner/components/cells/runner_type_cell.vue
deleted file mode 100644
index c8cb0bf6088..00000000000
--- a/app/assets/javascripts/runner/components/cells/runner_type_cell.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import RunnerTypeBadge from '../runner_type_badge.vue';
-import RunnerStateLockedBadge from '../runner_state_locked_badge.vue';
-import RunnerStatePausedBadge from '../runner_state_paused_badge.vue';
-import { I18N_LOCKED_RUNNER_DESCRIPTION, I18N_PAUSED_RUNNER_DESCRIPTION } from '../../constants';
-
-export default {
- components: {
- RunnerTypeBadge,
- RunnerStateLockedBadge,
- RunnerStatePausedBadge,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- runner: {
- type: Object,
- required: true,
- },
- },
- computed: {
- runnerType() {
- return this.runner.runnerType;
- },
- locked() {
- return this.runner.locked;
- },
- paused() {
- return !this.runner.active;
- },
- },
- i18n: {
- I18N_LOCKED_RUNNER_DESCRIPTION,
- I18N_PAUSED_RUNNER_DESCRIPTION,
- },
-};
-</script>
-
-<template>
- <div>
- <runner-type-badge :type="runnerType" size="sm" />
- <runner-state-locked-badge v-if="locked" size="sm" />
- <runner-state-paused-badge v-if="paused" size="sm" />
- </div>
-</template>
diff --git a/app/assets/javascripts/runner/components/helpers/masked_value.vue b/app/assets/javascripts/runner/components/helpers/masked_value.vue
deleted file mode 100644
index feccb37de81..00000000000
--- a/app/assets/javascripts/runner/components/helpers/masked_value.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- components: {
- GlButton,
- },
- props: {
- value: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- isMasked: true,
- };
- },
- computed: {
- label() {
- if (this.isMasked) {
- return __('Click to reveal');
- }
- return __('Click to hide');
- },
- icon() {
- if (this.isMasked) {
- return 'eye';
- }
- return 'eye-slash';
- },
- displayedValue() {
- if (this.isMasked && this.value?.length) {
- return '*'.repeat(this.value.length);
- }
- return this.value;
- },
- },
- methods: {
- toggleMasked() {
- this.isMasked = !this.isMasked;
- },
- },
-};
-</script>
-<template>
- <span
- >{{ displayedValue }}
- <gl-button
- :aria-label="label"
- :icon="icon"
- class="gl-text-body!"
- data-testid="toggle-masked"
- variant="link"
- @click="toggleMasked"
- />
- </span>
-</template>
diff --git a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue
new file mode 100644
index 00000000000..3fbe3c1be74
--- /dev/null
+++ b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue
@@ -0,0 +1,112 @@
+<script>
+import {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownForm,
+ GlDropdownItem,
+ GlDropdownDivider,
+} from '@gitlab/ui';
+import { s__ } from '~/locale';
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants';
+import RegistrationToken from './registration_token.vue';
+import RegistrationTokenResetDropdownItem from './registration_token_reset_dropdown_item.vue';
+
+export default {
+ i18n: {
+ showInstallationInstructions: s__(
+ 'Runners|Show runner installation and registration instructions',
+ ),
+ registrationToken: s__('Runners|Registration token'),
+ },
+ components: {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownForm,
+ GlDropdownItem,
+ GlDropdownDivider,
+ RegistrationToken,
+ RunnerInstructionsModal,
+ RegistrationTokenResetDropdownItem,
+ },
+ props: {
+ registrationToken: {
+ type: String,
+ required: true,
+ },
+ type: {
+ type: String,
+ required: true,
+ validator(type) {
+ return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type);
+ },
+ },
+ },
+ data() {
+ return {
+ currentRegistrationToken: this.registrationToken,
+ instructionsModalOpened: false,
+ };
+ },
+ computed: {
+ dropdownText() {
+ switch (this.type) {
+ case INSTANCE_TYPE:
+ return s__('Runners|Register an instance runner');
+ case GROUP_TYPE:
+ return s__('Runners|Register a group runner');
+ case PROJECT_TYPE:
+ return s__('Runners|Register a project runner');
+ default:
+ return s__('Runners|Register a runner');
+ }
+ },
+ },
+ methods: {
+ onShowInstructionsClick() {
+ // Rendering the modal on demand, to avoid
+ // loading instructions prematurely from API.
+ this.instructionsModalOpened = true;
+
+ this.$nextTick(() => {
+ // $refs.runnerInstructionsModal is defined in
+ // the tick after the modal is rendered
+ this.$refs.runnerInstructionsModal.show();
+ });
+ },
+ onTokenReset(token) {
+ this.currentRegistrationToken = token;
+
+ this.$refs.runnerRegistrationDropdown.hide(true);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ ref="runnerRegistrationDropdown"
+ menu-class="gl-w-auto!"
+ :text="dropdownText"
+ variant="confirm"
+ v-bind="$attrs"
+ >
+ <gl-dropdown-item @click.capture.native.stop="onShowInstructionsClick">
+ {{ $options.i18n.showInstallationInstructions }}
+ <runner-instructions-modal
+ v-if="instructionsModalOpened"
+ ref="runnerInstructionsModal"
+ :registration-token="registrationToken"
+ data-testid="runner-instructions-modal"
+ />
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-dropdown-form class="gl-p-4!">
+ <gl-form-group class="gl-mb-0" :label="$options.i18n.registrationToken">
+ <registration-token :value="currentRegistrationToken" />
+ </gl-form-group>
+ </gl-dropdown-form>
+ <gl-dropdown-divider />
+ <registration-token-reset-dropdown-item :type="type" @tokenReset="onTokenReset" />
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/runner/components/registration/registration_token.vue b/app/assets/javascripts/runner/components/registration/registration_token.vue
new file mode 100644
index 00000000000..d54a66ff0e4
--- /dev/null
+++ b/app/assets/javascripts/runner/components/registration/registration_token.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlButtonGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+export default {
+ components: {
+ GlButtonGroup,
+ GlButton,
+ ModalCopyButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isMasked: true,
+ };
+ },
+ computed: {
+ maskLabel() {
+ if (this.isMasked) {
+ return __('Click to reveal');
+ }
+ return __('Click to hide');
+ },
+ maskIcon() {
+ if (this.isMasked) {
+ return 'eye';
+ }
+ return 'eye-slash';
+ },
+ displayedValue() {
+ if (this.isMasked && this.value?.length) {
+ return '*'.repeat(this.value.length);
+ }
+ return this.value;
+ },
+ },
+ methods: {
+ onToggleMasked() {
+ this.isMasked = !this.isMasked;
+ },
+ onCopied() {
+ // value already in the clipboard, simply notify the user
+ this.$toast?.show(s__('Runners|Registration token copied!'));
+ },
+ },
+ i18n: {
+ copyLabel: s__('Runners|Copy registration token'),
+ },
+};
+</script>
+<template>
+ <gl-button-group>
+ <gl-button class="gl-font-monospace" data-testid="token-value" label>
+ {{ displayedValue }}
+ </gl-button>
+ <gl-button
+ v-gl-tooltip
+ :aria-label="maskLabel"
+ :title="maskLabel"
+ :icon="maskIcon"
+ class="gl-w-auto! gl-flex-shrink-0!"
+ data-testid="toggle-masked"
+ @click.stop="onToggleMasked"
+ />
+ <modal-copy-button
+ class="gl-w-auto! gl-flex-shrink-0!"
+ :aria-label="$options.i18n.copyLabel"
+ :title="$options.i18n.copyLabel"
+ :text="value"
+ @success="onCopied"
+ />
+ </gl-button-group>
+</template>
diff --git a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
new file mode 100644
index 00000000000..3bb15bff8d8
--- /dev/null
+++ b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
@@ -0,0 +1,113 @@
+<script>
+import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { __, s__ } from '~/locale';
+import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
+import { captureException } from '~/runner/sentry_utils';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants';
+
+export default {
+ name: 'RunnerRegistrationTokenReset',
+ components: {
+ GlDropdownItem,
+ GlLoadingIcon,
+ },
+ inject: {
+ groupId: {
+ default: null,
+ },
+ projectId: {
+ default: null,
+ },
+ },
+ props: {
+ type: {
+ type: String,
+ required: true,
+ validator(type) {
+ return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type);
+ },
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ computed: {
+ resetTokenInput() {
+ switch (this.type) {
+ case INSTANCE_TYPE:
+ return {
+ type: this.type,
+ };
+ case GROUP_TYPE:
+ return {
+ id: convertToGraphQLId(TYPE_GROUP, this.groupId),
+ type: this.type,
+ };
+ case PROJECT_TYPE:
+ return {
+ id: convertToGraphQLId(TYPE_PROJECT, this.projectId),
+ type: this.type,
+ };
+ default:
+ return null;
+ }
+ },
+ },
+ methods: {
+ async resetToken() {
+ // TODO Replace confirmation with gl-modal
+ // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333810
+ // eslint-disable-next-line no-alert
+ if (!window.confirm(__('Are you sure you want to reset the registration token?'))) {
+ return;
+ }
+
+ this.loading = true;
+ try {
+ const {
+ data: {
+ runnersRegistrationTokenReset: { token, errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: runnersRegistrationTokenResetMutation,
+ variables: {
+ input: this.resetTokenInput,
+ },
+ });
+ if (errors && errors.length) {
+ throw new Error(errors.join(' '));
+ }
+ this.onSuccess(token);
+ } catch (e) {
+ this.onError(e);
+ } finally {
+ this.loading = false;
+ }
+ },
+ onError(error) {
+ const { message } = error;
+ createFlash({ message });
+
+ this.reportToSentry(error);
+ },
+ onSuccess(token) {
+ this.$toast?.show(s__('Runners|New registration token generated!'));
+ this.$emit('tokenReset', token);
+ },
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown-item @click.capture.native.stop="resetToken">
+ {{ __('Reset registration token') }}
+ <gl-loading-icon v-if="loading" inline />
+ </gl-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/runner/components/runner_contacted_state_badge.vue b/app/assets/javascripts/runner/components/runner_contacted_state_badge.vue
new file mode 100644
index 00000000000..b4727f832f8
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_contacted_state_badge.vue
@@ -0,0 +1,69 @@
+<script>
+import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+import {
+ I18N_ONLINE_RUNNER_DESCRIPTION,
+ I18N_OFFLINE_RUNNER_DESCRIPTION,
+ I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
+ STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_NOT_CONNECTED,
+} from '../constants';
+
+export default {
+ components: {
+ GlBadge,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ runner: {
+ required: true,
+ type: Object,
+ },
+ },
+ computed: {
+ contactedAtTimeAgo() {
+ if (this.runner.contactedAt) {
+ return getTimeago().format(this.runner.contactedAt);
+ }
+ return null;
+ },
+ badge() {
+ switch (this.runner.status) {
+ case STATUS_ONLINE:
+ return {
+ variant: 'success',
+ label: s__('Runners|online'),
+ tooltip: sprintf(I18N_ONLINE_RUNNER_DESCRIPTION, {
+ timeAgo: this.contactedAtTimeAgo,
+ }),
+ };
+ case STATUS_OFFLINE:
+ return {
+ variant: 'muted',
+ label: s__('Runners|offline'),
+ tooltip: sprintf(I18N_OFFLINE_RUNNER_DESCRIPTION, {
+ timeAgo: this.contactedAtTimeAgo,
+ }),
+ };
+ case STATUS_NOT_CONNECTED:
+ return {
+ variant: 'muted',
+ label: s__('Runners|not connected'),
+ tooltip: I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
+ };
+ default:
+ return null;
+ }
+ },
+ },
+};
+</script>
+<template>
+ <gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" :variant="badge.variant" v-bind="$attrs">
+ {{ badge.label }}
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
index e04ca8ddca0..a9dfec35479 100644
--- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
@@ -2,6 +2,7 @@
import { cloneDeep } from 'lodash';
import { __ } from '~/locale';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import { searchValidator } from '~/runner/runner_search_utils';
import { CREATED_DESC, CREATED_ASC, CONTACTED_DESC, CONTACTED_ASC } from '../constants';
const sortOptions = [
@@ -31,9 +32,12 @@ export default {
value: {
type: Object,
required: true,
- validator(val) {
- return Array.isArray(val?.filters) && typeof val?.sort === 'string';
- },
+ validator: searchValidator,
+ },
+ tokens: {
+ type: Array,
+ required: false,
+ default: () => [],
},
namespace: {
type: String,
@@ -43,7 +47,7 @@ export default {
data() {
// filtered_search_bar_root.vue may mutate the inital
// filters. Use `cloneDeep` to prevent those mutations
- // from affecting this component
+ // from affecting this component
const { filters, sort } = cloneDeep(this.value);
return {
initialFilterValue: filters,
@@ -52,19 +56,17 @@ export default {
},
methods: {
onFilter(filters) {
- const { sort } = this.value;
-
+ // Apply new filters, from page 1
this.$emit('input', {
+ ...this.value,
filters,
- sort,
pagination: { page: 1 },
});
},
onSort(sort) {
- const { filters } = this.value;
-
+ // Apply new sort, from page 1
this.$emit('input', {
- filters,
+ ...this.value,
sort,
pagination: { page: 1 },
});
@@ -74,13 +76,16 @@ export default {
};
</script>
<template>
- <div>
+ <div
+ class="gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1"
+ >
<filtered-search
v-bind="$attrs"
:namespace="namespace"
recent-searches-storage-key="runners-search"
:sort-options="$options.sortOptions"
:initial-filter-value="initialFilterValue"
+ :tokens="tokens"
:initial-sort-by="initialSortBy"
:search-input-placeholder="__('Search or filter results...')"
data-testid="runners-filtered-search"
diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue
index 3f6ea389288..f8dbc469c22 100644
--- a/app/assets/javascripts/runner/components/runner_list.vue
+++ b/app/assets/javascripts/runner/components/runner_list.vue
@@ -1,12 +1,11 @@
<script>
import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { formatNumber, __, s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import { RUNNER_JOB_COUNT_LIMIT } from '../constants';
import RunnerActionsCell from './cells/runner_actions_cell.vue';
import RunnerSummaryCell from './cells/runner_summary_cell.vue';
-import RunnerTypeCell from './cells/runner_type_cell.vue';
+import RunnerStatusCell from './cells/runner_status_cell.vue';
import RunnerTags from './runner_tags.vue';
const tableField = ({ key, label = '', width = 10 }) => {
@@ -37,7 +36,7 @@ export default {
RunnerActionsCell,
RunnerSummaryCell,
RunnerTags,
- RunnerTypeCell,
+ RunnerStatusCell,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -54,18 +53,6 @@ export default {
},
},
methods: {
- formatProjectCount(projectCount) {
- if (projectCount === null) {
- return __('n/a');
- }
- return formatNumber(projectCount);
- },
- formatJobCount(jobCount) {
- if (jobCount > RUNNER_JOB_COUNT_LIMIT) {
- return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`;
- }
- return formatNumber(jobCount);
- },
runnerTrAttr(runner) {
if (runner) {
return {
@@ -76,13 +63,11 @@ export default {
},
},
fields: [
- tableField({ key: 'type', label: __('Type/State') }),
- tableField({ key: 'summary', label: s__('Runners|Runner'), width: 30 }),
+ tableField({ key: 'status', label: s__('Runners|Status') }),
+ tableField({ key: 'summary', label: s__('Runners|Runner ID'), width: 30 }),
tableField({ key: 'version', label: __('Version') }),
tableField({ key: 'ipAddress', label: __('IP Address') }),
- tableField({ key: 'projectCount', label: __('Projects'), width: 5 }),
- tableField({ key: 'jobCount', label: __('Jobs'), width: 5 }),
- tableField({ key: 'tagList', label: __('Tags') }),
+ tableField({ key: 'tagList', label: __('Tags'), width: 20 }),
tableField({ key: 'contactedAt', label: __('Last contact') }),
tableField({ key: 'actions', label: '' }),
],
@@ -103,8 +88,8 @@ export default {
<gl-skeleton-loader v-for="i in 4" :key="i" />
</template>
- <template #cell(type)="{ item }">
- <runner-type-cell :runner="item" />
+ <template #cell(status)="{ item }">
+ <runner-status-cell :runner="item" />
</template>
<template #cell(summary)="{ item, index }">
@@ -123,14 +108,6 @@ export default {
{{ ipAddress }}
</template>
- <template #cell(projectCount)="{ item: { projectCount } }">
- {{ formatProjectCount(projectCount) }}
- </template>
-
- <template #cell(jobCount)="{ item: { jobCount } }">
- {{ formatJobCount(jobCount) }}
- </template>
-
<template #cell(tagList)="{ item: { tagList } }">
<runner-tags :tag-list="tagList" size="sm" />
</template>
diff --git a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue b/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
deleted file mode 100644
index 475d362bb52..00000000000
--- a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<script>
-import { GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import MaskedValue from '~/runner/components/helpers/masked_value.vue';
-import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
-
-export default {
- components: {
- GlLink,
- GlSprintf,
- ClipboardButton,
- MaskedValue,
- RunnerInstructions,
- RunnerRegistrationTokenReset,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- inject: {
- runnerInstallHelpPage: {
- default: null,
- },
- },
- props: {
- registrationToken: {
- type: String,
- required: true,
- },
- type: {
- type: String,
- required: true,
- validator(type) {
- return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type);
- },
- },
- },
- data() {
- return {
- currentRegistrationToken: this.registrationToken,
- };
- },
- computed: {
- rootUrl() {
- return gon.gitlab_url || '';
- },
- typeName() {
- switch (this.type) {
- case INSTANCE_TYPE:
- return s__('Runners|shared');
- case GROUP_TYPE:
- return s__('Runners|group');
- case PROJECT_TYPE:
- return s__('Runners|specific');
- default:
- return '';
- }
- },
- },
- methods: {
- onTokenReset(token) {
- this.currentRegistrationToken = token;
- },
- },
-};
-</script>
-
-<template>
- <div class="bs-callout">
- <h5 data-testid="runner-help-title">
- <gl-sprintf :message="__('Set up a %{type} runner manually')">
- <template #type>
- {{ typeName }}
- </template>
- </gl-sprintf>
- </h5>
-
- <ol>
- <li>
- <gl-link :href="runnerInstallHelpPage" data-testid="runner-help-link" target="_blank">
- {{ __("Install GitLab Runner and ensure it's running.") }}
- </gl-link>
- </li>
- <li>
- {{ __('Register the runner with this URL:') }}
- <br />
-
- <code data-testid="coordinator-url">{{ rootUrl }}</code>
- <clipboard-button :title="__('Copy URL')" :text="rootUrl" />
- </li>
- <li>
- {{ __('And this registration token:') }}
- <br />
-
- <code data-testid="registration-token"
- ><masked-value :value="currentRegistrationToken"
- /></code>
- <clipboard-button :title="__('Copy token')" :text="currentRegistrationToken" />
- </li>
- </ol>
-
- <runner-registration-token-reset :type="type" @tokenReset="onTokenReset" />
-
- <runner-instructions />
- </div>
-</template>
diff --git a/app/assets/javascripts/runner/components/runner_state_paused_badge.vue b/app/assets/javascripts/runner/components/runner_paused_badge.vue
index d1e6fa05e4d..d1e6fa05e4d 100644
--- a/app/assets/javascripts/runner/components/runner_state_paused_badge.vue
+++ b/app/assets/javascripts/runner/components/runner_paused_badge.vue
diff --git a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
deleted file mode 100644
index cdf14abd4f9..00000000000
--- a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import createFlash, { FLASH_TYPES } from '~/flash';
-import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { __, s__ } from '~/locale';
-import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
-import { captureException } from '~/runner/sentry_utils';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
-
-export default {
- name: 'RunnerRegistrationTokenReset',
- components: {
- GlButton,
- },
- inject: {
- groupId: {
- default: null,
- },
- projectId: {
- default: null,
- },
- },
- props: {
- type: {
- type: String,
- required: true,
- validator(type) {
- return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type);
- },
- },
- },
- data() {
- return {
- loading: false,
- };
- },
- computed: {
- resetTokenInput() {
- switch (this.type) {
- case INSTANCE_TYPE:
- return {
- type: this.type,
- };
- case GROUP_TYPE:
- return {
- id: convertToGraphQLId(TYPE_GROUP, this.groupId),
- type: this.type,
- };
- case PROJECT_TYPE:
- return {
- id: convertToGraphQLId(TYPE_PROJECT, this.projectId),
- type: this.type,
- };
- default:
- return null;
- }
- },
- },
- methods: {
- async resetToken() {
- // TODO Replace confirmation with gl-modal
- // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333810
- // eslint-disable-next-line no-alert
- if (!window.confirm(__('Are you sure you want to reset the registration token?'))) {
- return;
- }
-
- this.loading = true;
- try {
- const {
- data: {
- runnersRegistrationTokenReset: { token, errors },
- },
- } = await this.$apollo.mutate({
- mutation: runnersRegistrationTokenResetMutation,
- variables: {
- input: this.resetTokenInput,
- },
- });
- if (errors && errors.length) {
- throw new Error(errors.join(' '));
- }
- this.onSuccess(token);
- } catch (e) {
- this.onError(e);
- } finally {
- this.loading = false;
- }
- },
- onError(error) {
- const { message } = error;
- createFlash({ message });
-
- this.reportToSentry(error);
- },
- onSuccess(token) {
- createFlash({
- message: s__('Runners|New registration token generated!'),
- type: FLASH_TYPES.SUCCESS,
- });
- this.$emit('tokenReset', token);
- },
- reportToSentry(error) {
- captureException({ error, component: this.$options.name });
- },
- },
-};
-</script>
-<template>
- <gl-button :loading="loading" @click="resetToken">
- {{ __('Reset registration token') }}
- </gl-button>
-</template>
diff --git a/app/assets/javascripts/runner/components/runner_state_locked_badge.vue b/app/assets/javascripts/runner/components/runner_state_locked_badge.vue
deleted file mode 100644
index 458526010bc..00000000000
--- a/app/assets/javascripts/runner/components/runner_state_locked_badge.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-<script>
-import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
-import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../constants';
-
-export default {
- components: {
- GlBadge,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- i18n: {
- I18N_LOCKED_RUNNER_DESCRIPTION,
- },
-};
-</script>
-<template>
- <gl-badge
- v-gl-tooltip="$options.i18n.I18N_LOCKED_RUNNER_DESCRIPTION"
- variant="warning"
- v-bind="$attrs"
- >
- {{ s__('Runners|locked') }}
- </gl-badge>
-</template>
diff --git a/app/assets/javascripts/runner/components/runner_tag.vue b/app/assets/javascripts/runner/components/runner_tag.vue
index 06562e618a8..6ad2023a866 100644
--- a/app/assets/javascripts/runner/components/runner_tag.vue
+++ b/app/assets/javascripts/runner/components/runner_tag.vue
@@ -1,11 +1,15 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlTooltipDirective, GlResizeObserverDirective } from '@gitlab/ui';
import { RUNNER_TAG_BADGE_VARIANT } from '../constants';
export default {
components: {
GlBadge,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ GlResizeObserver: GlResizeObserverDirective,
+ },
props: {
tag: {
type: String,
@@ -14,14 +18,39 @@ export default {
size: {
type: String,
required: false,
- default: 'md',
+ default: 'sm',
+ },
+ },
+ data() {
+ return {
+ overflowing: false,
+ };
+ },
+ computed: {
+ tooltip() {
+ if (this.overflowing) {
+ return this.tag;
+ }
+ return '';
+ },
+ },
+ methods: {
+ onResize() {
+ const { scrollWidth, offsetWidth } = this.$el;
+ this.overflowing = scrollWidth > offsetWidth;
},
},
RUNNER_TAG_BADGE_VARIANT,
};
</script>
<template>
- <gl-badge :size="size" :variant="$options.RUNNER_TAG_BADGE_VARIANT">
+ <gl-badge
+ v-gl-tooltip="tooltip"
+ v-gl-resize-observer="onResize"
+ class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ :size="size"
+ :variant="$options.RUNNER_TAG_BADGE_VARIANT"
+ >
{{ tag }}
</gl-badge>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_tags.vue b/app/assets/javascripts/runner/components/runner_tags.vue
index aec0d8e2c66..8da5e33076f 100644
--- a/app/assets/javascripts/runner/components/runner_tags.vue
+++ b/app/assets/javascripts/runner/components/runner_tags.vue
@@ -14,13 +14,19 @@ export default {
size: {
type: String,
required: false,
- default: 'md',
+ default: 'sm',
},
},
};
</script>
<template>
<div>
- <runner-tag v-for="tag in tagList" :key="tag" :tag="tag" :size="size" />
+ <runner-tag
+ v-for="tag in tagList"
+ :key="tag"
+ class="gl-display-inline gl-mr-1"
+ :tag="tag"
+ :size="size"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_type_alert.vue b/app/assets/javascripts/runner/components/runner_type_alert.vue
index aa435aaa823..1400875a1d6 100644
--- a/app/assets/javascripts/runner/components/runner_type_alert.vue
+++ b/app/assets/javascripts/runner/components/runner_type_alert.vue
@@ -9,17 +9,14 @@ const ALERT_DATA = {
message: s__(
'Runners|This runner is available to all groups and projects in your GitLab instance.',
),
- variant: 'success',
anchor: 'shared-runners',
},
[GROUP_TYPE]: {
message: s__('Runners|This runner is available to all projects and subgroups in a group.'),
- variant: 'success',
anchor: 'group-runners',
},
[PROJECT_TYPE]: {
message: s__('Runners|This runner is associated with one or more projects.'),
- variant: 'info',
anchor: 'specific-runners',
},
};
@@ -50,7 +47,7 @@ export default {
};
</script>
<template>
- <gl-alert v-if="alert" :variant="alert.variant" :dismissible="false">
+ <gl-alert v-if="alert" variant="info" :dismissible="false">
{{ alert.message }}
<gl-link :href="helpHref">{{ __('Learn more.') }}</gl-link>
</gl-alert>
diff --git a/app/assets/javascripts/runner/components/runner_type_badge.vue b/app/assets/javascripts/runner/components/runner_type_badge.vue
index 1a61b80184b..b885dcefdcb 100644
--- a/app/assets/javascripts/runner/components/runner_type_badge.vue
+++ b/app/assets/javascripts/runner/components/runner_type_badge.vue
@@ -12,17 +12,14 @@ import {
const BADGE_DATA = {
[INSTANCE_TYPE]: {
- variant: 'success',
text: s__('Runners|shared'),
tooltip: I18N_INSTANCE_RUNNER_DESCRIPTION,
},
[GROUP_TYPE]: {
- variant: 'success',
text: s__('Runners|group'),
tooltip: I18N_GROUP_RUNNER_DESCRIPTION,
},
[PROJECT_TYPE]: {
- variant: 'info',
text: s__('Runners|specific'),
tooltip: I18N_PROJECT_RUNNER_DESCRIPTION,
},
@@ -53,7 +50,7 @@ export default {
};
</script>
<template>
- <gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" :variant="badge.variant" v-bind="$attrs">
+ <gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" variant="info" v-bind="$attrs">
{{ badge.text }}
</gl-badge>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_type_tabs.vue b/app/assets/javascripts/runner/components/runner_type_tabs.vue
new file mode 100644
index 00000000000..b767dafaccf
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_type_tabs.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlTabs, GlTab } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { searchValidator } from '~/runner/runner_search_utils';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
+
+const tabs = [
+ {
+ title: s__('Runners|All'),
+ runnerType: null,
+ },
+ {
+ title: s__('Runners|Instance'),
+ runnerType: INSTANCE_TYPE,
+ },
+ {
+ title: s__('Runners|Group'),
+ runnerType: GROUP_TYPE,
+ },
+ {
+ title: s__('Runners|Project'),
+ runnerType: PROJECT_TYPE,
+ },
+];
+
+export default {
+ components: {
+ GlTabs,
+ GlTab,
+ },
+ props: {
+ value: {
+ type: Object,
+ required: true,
+ validator: searchValidator,
+ },
+ },
+ methods: {
+ onTabSelected({ runnerType }) {
+ this.$emit('input', {
+ ...this.value,
+ runnerType,
+ pagination: { page: 1 },
+ });
+ },
+ isTabActive({ runnerType }) {
+ return runnerType === this.value.runnerType;
+ },
+ },
+ tabs,
+};
+</script>
+<template>
+ <gl-tabs v-bind="$attrs" data-testid="runner-type-tabs">
+ <gl-tab
+ v-for="tab in $options.tabs"
+ :key="`${tab.runnerType}`"
+ :active="isTabActive(tab)"
+ @click="onTabSelected(tab)"
+ >
+ <template #title>
+ <slot name="title" :tab="tab">{{ tab.title }}</slot>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+</template>
diff --git a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
index 03dff5e61a5..9963048ae1d 100644
--- a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
+++ b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
@@ -10,23 +10,29 @@ import {
PARAM_KEY_STATUS,
} from '../../constants';
+const options = [
+ { value: STATUS_ACTIVE, title: s__('Runners|Active') },
+ { value: STATUS_PAUSED, title: s__('Runners|Paused') },
+ { value: STATUS_ONLINE, title: s__('Runners|Online') },
+ { value: STATUS_OFFLINE, title: s__('Runners|Offline') },
+ { value: STATUS_NOT_CONNECTED, title: s__('Runners|Not connected') },
+];
+
export const statusTokenConfig = {
icon: 'status',
title: __('Status'),
type: PARAM_KEY_STATUS,
token: BaseToken,
unique: true,
- options: [
- { value: STATUS_ACTIVE, title: s__('Runners|Active') },
- { value: STATUS_PAUSED, title: s__('Runners|Paused') },
- { value: STATUS_ONLINE, title: s__('Runners|Online') },
- { value: STATUS_OFFLINE, title: s__('Runners|Offline') },
-
- // Added extra quotes in this title to avoid splitting this value:
+ options: options.map(({ value, title }) => ({
+ value,
+ // Replace whitespace with a special character to avoid
+ // splitting this value.
+ // Replacing in each option, as translations may also
+ // contain spaces!
+ // see: https://gitlab.com/gitlab-org/gitlab/-/issues/344142
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
- { value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
- ],
- // TODO In principle we could support more complex search rules,
- // this can be added to a separate issue.
+ title: title.replace(' ', '\u00a0'),
+ })),
operators: OPERATOR_IS_ONLY,
};
diff --git a/app/assets/javascripts/runner/components/search_tokens/type_token_config.js b/app/assets/javascripts/runner/components/search_tokens/type_token_config.js
deleted file mode 100644
index 1da61c53386..00000000000
--- a/app/assets/javascripts/runner/components/search_tokens/type_token_config.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { __, s__ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
-import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, PARAM_KEY_RUNNER_TYPE } from '../../constants';
-
-export const typeTokenConfig = {
- icon: 'file-tree',
- title: __('Type'),
- type: PARAM_KEY_RUNNER_TYPE,
- token: BaseToken,
- unique: true,
- options: [
- { value: INSTANCE_TYPE, title: s__('Runners|instance') },
- { value: GROUP_TYPE, title: s__('Runners|group') },
- { value: PROJECT_TYPE, title: s__('Runners|project') },
- ],
- // TODO We should support more complex search rules,
- // search for multiple states (OR) or have NOT operators
- operators: OPERATOR_IS_ONLY,
-};
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index a2fb9d9efd8..3952e2398e0 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -1,21 +1,33 @@
import { s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
-export const RUNNER_JOB_COUNT_LIMIT = 1000;
export const GROUP_RUNNER_COUNT_LIMIT = 1000;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
+// Type
export const I18N_INSTANCE_RUNNER_DESCRIPTION = s__('Runners|Available to all projects');
export const I18N_GROUP_RUNNER_DESCRIPTION = s__(
'Runners|Available to all projects and subgroups in the group',
);
export const I18N_PROJECT_RUNNER_DESCRIPTION = s__('Runners|Associated with one or more projects');
+
+// Status
+export const I18N_ONLINE_RUNNER_DESCRIPTION = s__(
+ 'Runners|Runner is online; last contact was %{timeAgo}',
+);
+export const I18N_OFFLINE_RUNNER_DESCRIPTION = s__(
+ 'Runners|No recent contact from this runner; last contact was %{timeAgo}',
+);
+export const I18N_NOT_CONNECTED_RUNNER_DESCRIPTION = s__(
+ 'Runners|This runner has never connected to this instance',
+);
+
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects');
export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs');
-export const RUNNER_TAG_BADGE_VARIANT = 'info';
+export const RUNNER_TAG_BADGE_VARIANT = 'neutral';
export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100';
// Filtered search parameter names
diff --git a/app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql
new file mode 100644
index 00000000000..547cc43907c
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql
@@ -0,0 +1,14 @@
+#import "~/runner/graphql/runner_node.fragment.graphql"
+
+# Mutation for updates within the runners list via action
+# buttons (play, pause, ...), loads attributes shown in the
+# runner list.
+
+mutation runnerActionsUpdate($input: RunnerUpdateInput!) {
+ runnerUpdate(input: $input) {
+ runner {
+ ...RunnerNode
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
index 68d6f02f799..98f2dab26ca 100644
--- a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
@@ -10,6 +10,5 @@ fragment RunnerNode on CiRunner {
locked
tagList
contactedAt
- jobCount
- projectCount
+ status
}
diff --git a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
index dcc7fdf24f1..ea622fd4958 100644
--- a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
@@ -1,5 +1,8 @@
#import "ee_else_ce/runner/graphql/runner_details.fragment.graphql"
+# Mutation for updates from the runner form, loads
+# attributes shown in the runner details.
+
mutation runnerUpdate($input: RunnerUpdateInput!) {
runnerUpdate(input: $input) {
runner {
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index 4bb28796dfa..c3dfa885f27 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -5,14 +5,14 @@ import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber, sprintf, s__ } from '~/locale';
+import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
-import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerName from '../components/runner_name.vue';
import RunnerPagination from '../components/runner_pagination.vue';
+import RunnerTypeTabs from '../components/runner_type_tabs.vue';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
-import { typeTokenConfig } from '../components/search_tokens/type_token_config';
import {
I18N_FETCH_ERROR,
GROUP_FILTERED_SEARCH_NAMESPACE,
@@ -31,11 +31,12 @@ export default {
name: 'GroupRunnersApp',
components: {
GlLink,
+ RegistrationDropdown,
RunnerFilteredSearchBar,
RunnerList,
- RunnerManualSetupHelp,
RunnerName,
RunnerPagination,
+ RunnerTypeTabs,
},
props: {
registrationToken: {
@@ -112,7 +113,7 @@ export default {
});
},
searchTokens() {
- return [statusTokenConfig, typeTokenConfig];
+ return [statusTokenConfig];
},
filteredSearchNamespace() {
return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
@@ -144,7 +145,20 @@ export default {
<template>
<div>
- <runner-manual-setup-help :registration-token="registrationToken" :type="$options.GROUP_TYPE" />
+ <div class="gl-display-flex gl-align-items-center">
+ <runner-type-tabs
+ v-model="search"
+ content-class="gl-display-none"
+ nav-class="gl-border-none!"
+ />
+
+ <registration-dropdown
+ class="gl-ml-auto"
+ :registration-token="registrationToken"
+ :type="$options.GROUP_TYPE"
+ right
+ />
+ </div>
<runner-filtered-search-bar
v-model="search"
diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js
index 9545764c68d..60b7a7ab541 100644
--- a/app/assets/javascripts/runner/group_runners/index.js
+++ b/app/assets/javascripts/runner/group_runners/index.js
@@ -1,8 +1,10 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import GroupRunnersApp from './group_runners_app.vue';
+Vue.use(GlToast);
Vue.use(VueApollo);
export const initGroupRunners = (selector = '#js-group-runners') => {
@@ -21,12 +23,7 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
} = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/runner/runner_details/index.js b/app/assets/javascripts/runner/runner_details/index.js
index 05e6f86869d..db8f239a3c3 100644
--- a/app/assets/javascripts/runner/runner_details/index.js
+++ b/app/assets/javascripts/runner/runner_details/index.js
@@ -15,12 +15,7 @@ export const initRunnerDetail = (selector = '#js-runner-details') => {
const { runnerId } = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js
index 0a817ea0acf..b88023720e8 100644
--- a/app/assets/javascripts/runner/runner_search_utils.js
+++ b/app/assets/javascripts/runner/runner_search_utils.js
@@ -18,6 +18,50 @@ import {
RUNNER_PAGE_SIZE,
} from './constants';
+/**
+ * The filters and sorting of the runners are built around
+ * an object called "search" that contains the current state
+ * of search in the UI. For example:
+ *
+ * ```
+ * const search = {
+ * // The current tab
+ * runnerType: 'INSTANCE_TYPE',
+ *
+ * // Filters in the search bar
+ * filters: [
+ * { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
+ * { type: 'filtered-search-term', value: { data: '' } },
+ * ],
+ *
+ * // Current sorting value
+ * sort: 'CREATED_DESC',
+ *
+ * // Pagination information
+ * pagination: { page: 1 },
+ * };
+ * ```
+ *
+ * An object in this format can be used to generate URLs
+ * with the search parameters or by runner components
+ * a input using a v-model.
+ *
+ * @module runner_search_utils
+ */
+
+/**
+ * Validates a search value
+ * @param {Object} search
+ * @returns {boolean} True if the value follows the search format.
+ */
+export const searchValidator = ({ runnerType, filters, sort }) => {
+ return (
+ (runnerType === null || typeof runnerType === 'string') &&
+ Array.isArray(filters) &&
+ typeof sort === 'string'
+ );
+};
+
const getPaginationFromParams = (params) => {
const page = parseInt(params[PARAM_KEY_PAGE], 10);
const after = params[PARAM_KEY_AFTER];
@@ -35,13 +79,20 @@ const getPaginationFromParams = (params) => {
};
};
+/**
+ * Takes a URL query and transforms it into a "search" object
+ * @param {String?} query
+ * @returns {Object} A search object
+ */
export const fromUrlQueryToSearch = (query = window.location.search) => {
const params = queryToObject(query, { gatherArrays: true });
+ const runnerType = params[PARAM_KEY_RUNNER_TYPE]?.[0] || null;
return {
+ runnerType,
filters: prepareTokens(
urlQueryToFilter(query, {
- filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG],
+ filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_TAG],
filteredSearchTermKey: PARAM_KEY_SEARCH,
}),
),
@@ -50,8 +101,15 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
};
};
+/**
+ * Takes a "search" object and transforms it into a URL.
+ *
+ * @param {Object} search
+ * @param {String} url
+ * @returns {String} New URL for the page
+ */
export const fromSearchToUrl = (
- { filters = [], sort = null, pagination = {} },
+ { runnerType = null, filters = [], sort = null, pagination = {} },
url = window.location.href,
) => {
const filterParams = {
@@ -65,6 +123,10 @@ export const fromSearchToUrl = (
}),
};
+ if (runnerType) {
+ filterParams[PARAM_KEY_RUNNER_TYPE] = [runnerType];
+ }
+
if (!filterParams[PARAM_KEY_SEARCH]) {
filterParams[PARAM_KEY_SEARCH] = null;
}
@@ -82,21 +144,31 @@ export const fromSearchToUrl = (
return setUrlParams({ ...filterParams, ...otherParams }, url, false, true, true);
};
-export const fromSearchToVariables = ({ filters = [], sort = null, pagination = {} } = {}) => {
+/**
+ * Takes a "search" object and transforms it into variables for runner a GraphQL query.
+ *
+ * @param {Object} search
+ * @returns {Object} Hash of filter values
+ */
+export const fromSearchToVariables = ({
+ runnerType = null,
+ filters = [],
+ sort = null,
+ pagination = {},
+} = {}) => {
const variables = {};
const queryObj = filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH,
});
- variables.search = queryObj[PARAM_KEY_SEARCH];
-
- // TODO Get more than one value when GraphQL API supports OR for "status" or "runner_type"
[variables.status] = queryObj[PARAM_KEY_STATUS] || [];
- [variables.type] = queryObj[PARAM_KEY_RUNNER_TYPE] || [];
-
+ variables.search = queryObj[PARAM_KEY_SEARCH];
variables.tagList = queryObj[PARAM_KEY_TAG];
+ if (runnerType) {
+ variables.type = runnerType;
+ }
if (sort) {
variables.sort = sort;
}
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 99cf16c8350..5c7cbeac5b2 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -13,9 +13,9 @@ export default {
ConfidentialityFilter,
},
computed: {
- ...mapState(['query']),
+ ...mapState(['urlQuery', 'sidebarDirty']),
showReset() {
- return this.query.state || this.query.confidential;
+ return this.urlQuery.state || this.urlQuery.confidential;
},
},
methods: {
@@ -32,7 +32,7 @@ export default {
<status-filter />
<confidentiality-filter />
<div class="gl-display-flex gl-align-items-center gl-mt-3">
- <gl-button category="primary" variant="confirm" size="small" type="submit">
+ <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
{{ __('Apply') }}
</gl-button>
<gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{
diff --git a/app/assets/javascripts/search/sidebar/components/radio_filter.vue b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
index 73911b9d319..aa7c26b8044 100644
--- a/app/assets/javascripts/search/sidebar/components/radio_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
@@ -1,7 +1,7 @@
<script>
import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
-import { sprintf, s__ } from '~/locale';
+import { sprintf, __ } from '~/locale';
export default {
name: 'RadioFilter',
@@ -49,7 +49,7 @@ export default {
...mapActions(['setQuery']),
radioLabel(filter) {
return filter.value === this.ANY.value
- ? sprintf(s__('Any %{header}'), { header: this.filterData.header.toLowerCase() })
+ ? sprintf(__('Any %{header}'), { header: this.filterData.header.toLowerCase() })
: filter.label;
},
},
diff --git a/app/assets/javascripts/search/sidebar/constants/state_filter_data.js b/app/assets/javascripts/search/sidebar/constants/state_filter_data.js
index 7c9a029ffe4..2f9f8a7cb46 100644
--- a/app/assets/javascripts/search/sidebar/constants/state_filter_data.js
+++ b/app/assets/javascripts/search/sidebar/constants/state_filter_data.js
@@ -5,7 +5,7 @@ const header = __('Status');
const filters = {
ANY: {
label: __('Any'),
- value: 'all',
+ value: null,
},
OPEN: {
label: __('Open'),
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index be64a9278e3..a6af5644681 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -2,9 +2,9 @@ import Api from '~/api';
import createFlash from '~/flash';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
+import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, SIDEBAR_PARAMS } from './constants';
import * as types from './mutation_types';
-import { loadDataFromLS, setFrequentItemToLS, mergeById } from './utils';
+import { loadDataFromLS, setFrequentItemToLS, mergeById, isSidebarDirty } from './utils';
export const fetchGroups = ({ commit }, search) => {
commit(types.REQUEST_GROUPS);
@@ -86,8 +86,12 @@ export const setFrequentProject = ({ state, commit }, item) => {
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: frequentItems });
};
-export const setQuery = ({ commit }, { key, value }) => {
+export const setQuery = ({ state, commit }, { key, value }) => {
commit(types.SET_QUERY, { key, value });
+
+ if (SIDEBAR_PARAMS.includes(key)) {
+ commit(types.SET_SIDEBAR_DIRTY, isSidebarDirty(state.query, state.urlQuery));
+ }
};
export const applyQuery = ({ state }) => {
diff --git a/app/assets/javascripts/search/store/constants.js b/app/assets/javascripts/search/store/constants.js
index 3abf7cac6ba..678bd82c7a6 100644
--- a/app/assets/javascripts/search/store/constants.js
+++ b/app/assets/javascripts/search/store/constants.js
@@ -1,3 +1,6 @@
+import { stateFilterData } from '~/search/sidebar/constants/state_filter_data';
+import { confidentialFilterData } from '~/search/sidebar/constants/confidential_filter_data';
+
export const MAX_FREQUENT_ITEMS = 5;
export const MAX_FREQUENCY = 5;
@@ -5,3 +8,5 @@ export const MAX_FREQUENCY = 5;
export const GROUPS_LOCAL_STORAGE_KEY = 'global-search-frequent-groups';
export const PROJECTS_LOCAL_STORAGE_KEY = 'global-search-frequent-projects';
+
+export const SIDEBAR_PARAMS = [stateFilterData.filterParam, confidentialFilterData.filterParam];
diff --git a/app/assets/javascripts/search/store/mutation_types.js b/app/assets/javascripts/search/store/mutation_types.js
index 5c1c29dc738..bf1e3e79cba 100644
--- a/app/assets/javascripts/search/store/mutation_types.js
+++ b/app/assets/javascripts/search/store/mutation_types.js
@@ -7,5 +7,6 @@ export const RECEIVE_PROJECTS_SUCCESS = 'RECEIVE_PROJECTS_SUCCESS';
export const RECEIVE_PROJECTS_ERROR = 'RECEIVE_PROJECTS_ERROR';
export const SET_QUERY = 'SET_QUERY';
+export const SET_SIDEBAR_DIRTY = 'SET_SIDEBAR_DIRTY';
export const LOAD_FREQUENT_ITEMS = 'LOAD_FREQUENT_ITEMS';
diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js
index 63156a89738..5d154fe3aa0 100644
--- a/app/assets/javascripts/search/store/mutations.js
+++ b/app/assets/javascripts/search/store/mutations.js
@@ -26,6 +26,9 @@ export default {
[types.SET_QUERY](state, { key, value }) {
state.query[key] = value;
},
+ [types.SET_SIDEBAR_DIRTY](state, value) {
+ state.sidebarDirty = value;
+ },
[types.LOAD_FREQUENT_ITEMS](state, { key, data }) {
state.frequentItems[key] = data;
},
diff --git a/app/assets/javascripts/search/store/state.js b/app/assets/javascripts/search/store/state.js
index 5b1429ccc97..d4005697f35 100644
--- a/app/assets/javascripts/search/store/state.js
+++ b/app/assets/javascripts/search/store/state.js
@@ -1,6 +1,8 @@
+import { cloneDeep } from 'lodash';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
const createState = ({ query }) => ({
+ urlQuery: cloneDeep(query),
query,
groups: [],
fetchingGroups: false,
@@ -10,5 +12,6 @@ const createState = ({ query }) => ({
[GROUPS_LOCAL_STORAGE_KEY]: [],
[PROJECTS_LOCAL_STORAGE_KEY]: [],
},
+ sidebarDirty: false,
});
export default createState;
diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js
index b00b9bb0f2e..6b56ff0b5e5 100644
--- a/app/assets/javascripts/search/store/utils.js
+++ b/app/assets/javascripts/search/store/utils.js
@@ -1,5 +1,5 @@
import AccessorUtilities from '../../lib/utils/accessor';
-import { MAX_FREQUENT_ITEMS, MAX_FREQUENCY } from './constants';
+import { MAX_FREQUENT_ITEMS, MAX_FREQUENCY, SIDEBAR_PARAMS } from './constants';
function extractKeys(object, keyList) {
return Object.fromEntries(keyList.map((key) => [key, object[key]]));
@@ -80,3 +80,13 @@ export const mergeById = (inflatedData, storedData) => {
return { ...stored, ...data };
});
};
+
+export const isSidebarDirty = (currentQuery, urlQuery) => {
+ return SIDEBAR_PARAMS.some((param) => {
+ // userAddParam ensures we don't get a false dirty from null !== undefined
+ const userAddedParam = !urlQuery[param] && currentQuery[param];
+ const userChangedExistingParam = urlQuery[param] && urlQuery[param] !== currentQuery[param];
+
+ return userAddedParam || userChangedExistingParam;
+ });
+};
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index 6c70a8c33db..bc13150c99c 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlTab, GlTabs, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
@@ -31,6 +31,7 @@ export default {
AutoDevOpsAlert,
AutoDevOpsEnabledAlert,
FeatureCard,
+ GlAlert,
GlLink,
GlSprintf,
GlTab,
@@ -79,6 +80,7 @@ export default {
data() {
return {
autoDevopsEnabledAlertDismissedProjects: [],
+ errorMessage: '',
};
},
computed: {
@@ -106,6 +108,12 @@ export default {
dismissedProjects.add(this.projectPath);
this.autoDevopsEnabledAlertDismissedProjects = Array.from(dismissedProjects);
},
+ onError(message) {
+ this.errorMessage = message;
+ },
+ dismissAlert() {
+ this.errorMessage = '';
+ },
},
autoDevopsEnabledAlertStorageKey: AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
};
@@ -113,6 +121,16 @@ export default {
<template>
<article>
+ <gl-alert
+ v-if="errorMessage"
+ sticky
+ class="gl-top-8 gl-z-index-1"
+ data-testid="manage-via-mr-error-alert"
+ variant="danger"
+ @dismiss="dismissAlert"
+ >
+ {{ errorMessage }}
+ </gl-alert>
<local-storage-sync
v-model="autoDevopsEnabledAlertDismissedProjects"
:storage-key="$options.autoDevopsEnabledAlertStorageKey"
@@ -174,6 +192,7 @@ export default {
data-testid="security-testing-card"
:feature="feature"
class="gl-mb-6"
+ @error="onError"
/>
</template>
</section-layout>
@@ -207,6 +226,7 @@ export default {
:key="feature.type"
:feature="feature"
class="gl-mb-6"
+ @error="onError"
/>
</template>
</section-layout>
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index 6a282df99bf..9c80506549e 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -3,6 +3,7 @@ import { __, s__ } from '~/locale';
import {
REPORT_TYPE_SAST,
+ REPORT_TYPE_SAST_IAC,
REPORT_TYPE_DAST,
REPORT_TYPE_DAST_PROFILES,
REPORT_TYPE_SECRET_DETECTION,
@@ -16,6 +17,7 @@ import {
} from '~/vue_shared/security_reports/constants';
import configureSastMutation from '../graphql/configure_sast.mutation.graphql';
+import configureSastIacMutation from '../graphql/configure_iac.mutation.graphql';
import configureSecretDetectionMutation from '../graphql/configure_secret_detection.mutation.graphql';
/**
@@ -30,6 +32,19 @@ export const SAST_CONFIG_HELP_PATH = helpPagePath('user/application_security/sas
anchor: 'configuration',
});
+export const SAST_IAC_NAME = __('Infrastructure as Code (IaC) Scanning');
+export const SAST_IAC_SHORT_NAME = s__('ciReport|IaC Scanning');
+export const SAST_IAC_DESCRIPTION = __(
+ 'Analyze your infrastructure as code configuration files for known vulnerabilities.',
+);
+export const SAST_IAC_HELP_PATH = helpPagePath('user/application_security/iac_scanning/index');
+export const SAST_IAC_CONFIG_HELP_PATH = helpPagePath(
+ 'user/application_security/iac_scanning/index',
+ {
+ anchor: 'configuration',
+ },
+);
+
export const DAST_NAME = __('Dynamic Application Security Testing (DAST)');
export const DAST_SHORT_NAME = s__('ciReport|DAST');
export const DAST_DESCRIPTION = __('Analyze a review version of your web application.');
@@ -141,6 +156,27 @@ export const securityFeatures = [
// https://gitlab.com/gitlab-org/gitlab/-/issues/331621
canEnableByMergeRequest: true,
},
+ ...(gon?.features?.configureIacScanningViaMr
+ ? [
+ {
+ name: SAST_IAC_NAME,
+ shortName: SAST_IAC_SHORT_NAME,
+ description: SAST_IAC_DESCRIPTION,
+ helpPath: SAST_IAC_HELP_PATH,
+ configurationHelpPath: SAST_IAC_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_SAST_IAC,
+
+ // This field is currently hardcoded because SAST IaC is always available.
+ // It will eventually come from the Backend, the progress is tracked in
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/331622
+ available: true,
+
+ // This field will eventually come from the backend, the progress is
+ // tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/331621
+ canEnableByMergeRequest: true,
+ },
+ ]
+ : []),
{
name: DAST_NAME,
shortName: DAST_SHORT_NAME,
@@ -242,6 +278,21 @@ export const featureToMutationMap = {
},
}),
},
+ ...(gon?.features?.configureIacScanningViaMr
+ ? {
+ [REPORT_TYPE_SAST_IAC]: {
+ mutationId: 'configureSastIac',
+ getMutationPayload: (projectPath) => ({
+ mutation: configureSastIacMutation,
+ variables: {
+ input: {
+ projectPath,
+ },
+ },
+ }),
+ },
+ }
+ : {}),
[REPORT_TYPE_SECRET_DETECTION]: {
mutationId: 'configureSecretDetection',
getMutationPayload: (projectPath) => ({
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
index 86afdbfeb8c..33d72b54f86 100644
--- a/app/assets/javascripts/security_configuration/components/feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -66,6 +66,11 @@ export default {
return Boolean(name && description && configurationText);
},
},
+ methods: {
+ onError(message) {
+ this.$emit('error', message);
+ },
+ },
i18n: {
enabled: s__('SecurityConfiguration|Enabled'),
notEnabled: s__('SecurityConfiguration|Not enabled'),
@@ -129,6 +134,7 @@ export default {
category="primary"
class="gl-mt-5"
:data-qa-selector="`${feature.type}_mr_button`"
+ @error="onError"
/>
<gl-button
diff --git a/app/assets/javascripts/security_configuration/graphql/configure_iac.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/configure_iac.mutation.graphql
new file mode 100644
index 00000000000..26b826ef722
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/graphql/configure_iac.mutation.graphql
@@ -0,0 +1,6 @@
+mutation configureSastIac($input: ConfigureSastIacInput!) {
+ configureSastIac(input: $input) {
+ successPath
+ errors
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index 60d2c0d4e5a..a8623b468f2 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -14,7 +14,7 @@ export const initSecurityConfiguration = (el) => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
const {
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index b1c8f6ef22e..0021fe909e5 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -339,7 +339,7 @@ export default {
</div>
<div class="gl-display-flex">
<span class="gl-text-gray-600 gl-ml-5">
- {{ s__('SetStatusModal|A busy indicator is shown next to your name and avatar.') }}
+ {{ s__('SetStatusModal|An indicator appears next to your name and avatar') }}
</span>
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index e41bb41dc05..bdd014163a0 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -39,6 +39,9 @@ export default {
assignSelf() {
this.$emit('assign-self');
},
+ toggleAttentionRequested(data) {
+ this.$emit('toggle-attention-requested', data);
+ },
},
};
</script>
@@ -58,7 +61,12 @@ export default {
</template>
</span>
- <uncollapsed-assignee-list v-else :users="sortedAssigness" :issuable-type="issuableType" />
+ <uncollapsed-assignee-list
+ v-else
+ :users="sortedAssigness"
+ :issuable-type="issuableType"
+ @toggle-attention-requested="toggleAttentionRequested"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
index 80caebad39d..a3379784bc1 100644
--- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
@@ -32,6 +32,11 @@ export default {
return this.users.length === 0;
},
},
+ methods: {
+ toggleAttentionRequested(data) {
+ this.$emit('toggle-attention-requested', data);
+ },
+ },
};
</script>
@@ -61,6 +66,7 @@ export default {
:users="users"
:issuable-type="issuableType"
class="gl-text-gray-800 gl-mt-2 hide-collapsed"
+ @toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index c6877226b7d..453dd1b0580 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -125,6 +125,9 @@ export default {
availability: this.assigneeAvailabilityStatus[username] || '',
}));
},
+ toggleAttentionRequested(data) {
+ this.mediator.toggleAttentionRequested('assignee', data);
+ },
},
};
</script>
@@ -152,6 +155,7 @@ export default {
:editable="store.editable"
:issuable-type="issuableType"
@assign-self="assignSelf"
+ @toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
index c2ca87af9ce..8d5c3b2def3 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -1,6 +1,8 @@
<script>
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IssuableType } from '~/issue_show/constants';
import { __, sprintf } from '~/locale';
+import AttentionRequestedToggle from '../attention_requested_toggle.vue';
import AssigneeAvatarLink from './assignee_avatar_link.vue';
import UserNameWithStatus from './user_name_with_status.vue';
@@ -8,9 +10,11 @@ const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
+ AttentionRequestedToggle,
AssigneeAvatarLink,
UserNameWithStatus,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
users: {
type: Array,
@@ -32,6 +36,10 @@ export default {
return this.users[0];
},
hasOneUser() {
+ if (this.showVerticalList) {
+ return false;
+ }
+
return this.users.length === 1;
},
hiddenAssigneesLabel() {
@@ -45,6 +53,10 @@ export default {
return this.users.length - DEFAULT_RENDER_COUNT;
},
uncollapsedUsers() {
+ if (this.showVerticalList) {
+ return this.users;
+ }
+
const uncollapsedLength = this.showLess
? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
: this.users.length;
@@ -53,6 +65,12 @@ export default {
username() {
return `@${this.firstUser.username}`;
},
+ showVerticalList() {
+ return this.glFeatures.mrAttentionRequests && this.isMergeRequest;
+ },
+ isMergeRequest() {
+ return this.issuableType === IssuableType.MergeRequest;
+ },
},
methods: {
toggleShowLess() {
@@ -64,6 +82,9 @@ export default {
}
return u?.status?.availability || '';
},
+ toggleAttentionRequested(data) {
+ this.$emit('toggle-attention-requested', data);
+ },
},
};
</script>
@@ -84,11 +105,34 @@ export default {
<div v-else>
<div class="gl-display-flex gl-flex-wrap">
<div
- v-for="user in uncollapsedUsers"
+ v-for="(user, index) in uncollapsedUsers"
:key="user.id"
- class="user-item gl-display-inline-block"
+ :class="{
+ 'user-item': !showVerticalList,
+ 'gl-mb-3': index !== users.length - 1 && showVerticalList,
+ }"
+ class="gl-display-inline-block"
>
- <assignee-avatar-link :user="user" :issuable-type="issuableType" />
+ <attention-requested-toggle
+ v-if="showVerticalList && user.can_update_merge_request"
+ :user="user"
+ type="assignee"
+ @toggle-attention-requested="toggleAttentionRequested"
+ />
+ <assignee-avatar-link
+ :user="user"
+ :issuable-type="issuableType"
+ :tooltip-has-name="!showVerticalList"
+ >
+ <div
+ v-if="showVerticalList"
+ class="gl-ml-3 gl-line-height-normal gl-display-grid"
+ data-testid="username"
+ >
+ <user-name-with-status :name="user.name" :availability="userAvailability(user)" />
+ <span>@{{ user.username }}</span>
+ </div>
+ </assignee-avatar-link>
</div>
</div>
<div v-if="renderShowMoreSection" class="user-list-more gl-hover-text-blue-800">
diff --git a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
new file mode 100644
index 00000000000..38ba468d197
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
@@ -0,0 +1,74 @@
+<script>
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+
+export default {
+ i18n: {
+ attentionRequestedReviewer: __('Request attention to review'),
+ attentionRequestedAssignee: __('Request attention'),
+ removeAttentionRequested: __('Remove attention request'),
+ },
+ components: {
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ type: {
+ type: String,
+ required: true,
+ },
+ user: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ computed: {
+ tooltipTitle() {
+ if (this.user.attention_requested) {
+ return this.$options.i18n.removeAttentionRequested;
+ }
+
+ return this.type === 'reviewer'
+ ? this.$options.i18n.attentionRequestedReviewer
+ : this.$options.i18n.attentionRequestedAssignee;
+ },
+ },
+ methods: {
+ toggleAttentionRequired() {
+ if (this.loading) return;
+
+ this.$root.$emit(BV_HIDE_TOOLTIP);
+ this.loading = true;
+ this.$emit('toggle-attention-requested', {
+ user: this.user,
+ callback: this.toggleAttentionRequiredComplete,
+ });
+ },
+ toggleAttentionRequiredComplete() {
+ this.loading = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <span v-gl-tooltip.left.viewport="tooltipTitle">
+ <gl-button
+ :loading="loading"
+ :variant="user.attention_requested ? 'warning' : 'default'"
+ :icon="user.attention_requested ? 'star' : 'star-o'"
+ :aria-label="tooltipTitle"
+ size="small"
+ category="tertiary"
+ @click="toggleAttentionRequired"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
index d5647619ea3..5cd4a1a5192 100644
--- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -11,6 +11,7 @@ import { toLabelGid } from '~/sidebar/utils';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const mutationMap = {
@@ -48,6 +49,7 @@ export default {
return {
isLabelsSelectInProgress: false,
selectedLabels: this.initiallySelectedLabels,
+ LabelType,
};
},
methods: {
@@ -154,13 +156,12 @@ export default {
:footer-manage-label-title="__('Manage project labels')"
:labels-create-title="__('Create project label')"
:labels-filter-base-path="projectIssuesPath"
- :labels-select-in-progress="isLabelsSelectInProgress"
- :selected-labels="selectedLabels"
:variant="$options.variant"
:issuable-type="issuableType"
+ workspace-type="project"
+ :attr-workspace-path="fullPath"
+ :label-create-type="LabelType.project"
data-qa-selector="labels_block"
- @onLabelRemove="handleLabelRemove"
- @updateSelectedLabels="handleUpdateSelectedLabels"
>
{{ __('None') }}
</labels-select-widget>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
index 5729b958b5d..b07fd944ff9 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -49,6 +49,9 @@ export default {
requestReview(data) {
this.$emit('request-review', data);
},
+ toggleAttentionRequested(data) {
+ this.$emit('toggle-attention-requested', data);
+ },
},
};
</script>
@@ -70,6 +73,7 @@ export default {
:root-path="rootPath"
:issuable-type="issuableType"
@request-review="requestReview"
+ @toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
index e414aaf719b..2ea63219e92 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -88,6 +88,9 @@ export default {
requestReview(data) {
this.mediator.requestReview(data);
},
+ toggleAttentionRequested(data) {
+ this.mediator.toggleAttentionRequested('reviewer', data);
+ },
},
};
</script>
@@ -106,6 +109,7 @@ export default {
:editable="store.editable"
:issuable-type="issuableType"
@request-review="requestReview"
+ @toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
index 2922008cfb2..adaf1b65f3f 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
@@ -1,6 +1,8 @@
<script>
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, sprintf, s__ } from '~/locale';
+import AttentionRequestedToggle from '../attention_requested_toggle.vue';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
const LOADING_STATE = 'loading';
@@ -14,10 +16,12 @@ export default {
GlButton,
GlIcon,
ReviewerAvatarLink,
+ AttentionRequestedToggle,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
users: {
type: Array,
@@ -76,6 +80,9 @@ export default {
this.loadingStates[userId] = null;
}
},
+ toggleAttentionRequested(data) {
+ this.$emit('toggle-attention-requested', data);
+ },
},
LOADING_STATE,
SUCCESS_STATE,
@@ -90,6 +97,12 @@ export default {
:class="{ 'gl-mb-3': index !== users.length - 1 }"
data-testid="reviewer"
>
+ <attention-requested-toggle
+ v-if="glFeatures.mrAttentionRequests && user.can_update_merge_request"
+ :user="user"
+ type="reviewer"
+ @toggle-attention-requested="toggleAttentionRequested"
+ />
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType">
<div class="gl-ml-3 gl-line-height-normal gl-display-grid">
<span>{{ user.name }}</span>
@@ -113,7 +126,9 @@ export default {
data-testid="re-request-success"
/>
<gl-button
- v-else-if="user.can_update_merge_request && user.reviewed"
+ v-else-if="
+ user.can_update_merge_request && user.reviewed && !glFeatures.mrAttentionRequests
+ "
v-gl-tooltip.left
:title="$options.i18n.reRequestReview"
:aria-label="$options.i18n.reRequestReview"
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 8f4d5406da8..0ba8c4f8907 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -370,6 +370,7 @@ export default {
:loading="loading"
class="gl-w-full"
toggle-class="gl-max-w-100"
+ block
@shown="setFocus"
>
<gl-search-box-by-type ref="search" v-model="searchTerm" />
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index 22adbd79ef6..056b3e98a1c 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -134,7 +134,7 @@ export default {
v-if="canUpdate && !initialLoading && canEdit"
category="tertiary"
size="small"
- class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2"
+ class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2 shortcut-sidebar-dropdown-toggle"
data-testid="edit-button"
:data-track-action="tracking.event"
:data-track-label="tracking.label"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index d4a8abb81a8..5d4031ac68b 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
import createFlash from '~/flash';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -12,7 +12,7 @@ const TIME_DATE_FORMAT = 'mmmm d, yyyy, HH:MM ("UTC:" o)';
export default {
components: {
GlLoadingIcon,
- GlTable,
+ GlTableLite,
},
inject: ['issuableType'],
props: {
@@ -89,7 +89,7 @@ export default {
<template>
<div>
<div v-if="isLoading"><gl-loading-icon size="md" /></div>
- <gl-table v-else :items="report" :fields="$options.fields" foot-clone>
+ <gl-table-lite v-else :items="report" :fields="$options.fields" foot-clone>
<template #cell(spentAt)="{ item: { spentAt } }">
<div>{{ formatDate(spentAt) }}</div>
</template>
@@ -111,6 +111,6 @@ export default {
<div>{{ getSummary(summary, note) }}</div>
</template>
<template #foot(note)>&nbsp;</template>
- </gl-table>
+ </gl-table-lite>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index e593973da82..ac34a75ac5c 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,4 +1,5 @@
-import { IssuableType } from '~/issue_show/constants';
+import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
+import { IssuableType, WorkspaceType } from '~/issue_show/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
@@ -29,11 +30,14 @@ import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_conf
import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql';
import mergeRequestMilestoneMutation from '~/sidebar/queries/update_merge_request_milestone.mutation.graphql';
+import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql';
import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
import epicLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql';
+import updateEpicLabelsMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
import groupLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql';
import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
+import mergeRequestLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql';
import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql';
import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
@@ -109,14 +113,30 @@ export const referenceQueries = {
},
};
-export const labelsQueries = {
+export const workspaceLabelsQueries = {
+ [WorkspaceType.project]: {
+ query: projectLabelsQuery,
+ },
+ [WorkspaceType.group]: {
+ query: groupLabelsQuery,
+ },
+};
+
+export const issuableLabelsQueries = {
[IssuableType.Issue]: {
issuableQuery: issueLabelsQuery,
- workspaceQuery: projectLabelsQuery,
+ mutation: updateIssueLabelsMutation,
+ mutationName: 'updateIssue',
+ },
+ [IssuableType.MergeRequest]: {
+ issuableQuery: mergeRequestLabelsQuery,
+ mutation: updateMergeRequestLabelsMutation,
+ mutationName: 'mergeRequestSetLabels',
},
[IssuableType.Epic]: {
issuableQuery: epicLabelsQuery,
- workspaceQuery: groupLabelsQuery,
+ mutation: updateEpicLabelsMutation,
+ mutationName: 'updateEpic',
},
};
diff --git a/app/assets/javascripts/sidebar/graphql.js b/app/assets/javascripts/sidebar/graphql.js
index 1a806a051b7..6a670db2d38 100644
--- a/app/assets/javascripts/sidebar/graphql.js
+++ b/app/assets/javascripts/sidebar/graphql.js
@@ -25,7 +25,6 @@ export const defaultClient = createDefaultClient(resolvers, {
cacheConfig: {
fragmentMatcher,
},
- assumeImmutableResults: true,
});
export const apolloProvider = new VueApollo({
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 9f5a2f4ebb0..898be4a97ce 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -260,6 +260,10 @@ export function mountSidebarLabels() {
variant: DropdownVariant.Sidebar,
canUpdate: parseBoolean(el.dataset.canEdit),
isClassicSidebar: true,
+ issuableType:
+ isInIssuePage() || isInIncidentPage() || isInDesignPage()
+ ? IssuableType.Issue
+ : IssuableType.MergeRequest,
},
render: (createElement) => createElement(SidebarLabels),
});
diff --git a/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql b/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql
index 2aff7da4605..dd85eb1631b 100644
--- a/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql
@@ -1,4 +1,4 @@
-query($fullPath: ID!, $iid: String!) {
+query sidebarDetails($fullPath: ID!, $iid: String!) {
project(fullPath: $fullPath) {
issue(iid: $iid) {
iid
diff --git a/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql b/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql
new file mode 100644
index 00000000000..a9f4af6e1b9
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql
@@ -0,0 +1,7 @@
+mutation mergeRequestToggleAttentionRequested($projectPath: ID!, $iid: String!, $userId: ID!) {
+ mergeRequestToggleAttentionRequested(
+ input: { projectPath: $projectPath, iid: $iid, userId: $userId }
+ ) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql b/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql
index 28a47735143..2c6f379744e 100644
--- a/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql
@@ -1,4 +1,4 @@
-mutation($projectPath: ID!, $iid: String!, $healthStatus: HealthStatus) {
+mutation updateIssueHealthStatus($projectPath: ID!, $iid: String!, $healthStatus: HealthStatus) {
updateIssue(input: { projectPath: $projectPath, iid: $iid, healthStatus: $healthStatus }) {
issuable: issue {
id
diff --git a/app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql
index 69927ddd205..2d58843140f 100644
--- a/app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql
@@ -1,4 +1,4 @@
-mutation updateEpic($input: UpdateEpicInput!) {
+mutation updateIssuableConfidential($input: UpdateEpicInput!) {
issuableSetConfidential: updateEpic(input: $input) {
issuable: epic {
id
diff --git a/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql
index af43766aed5..4a3090f3836 100644
--- a/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql
@@ -1,4 +1,4 @@
-mutation epicSetSubscription($fullPath: ID!, $iid: ID!, $subscribedState: Boolean!) {
+mutation boardEpicSetSubscription($fullPath: ID!, $iid: ID!, $subscribedState: Boolean!) {
updateIssuableSubscription: epicSetSubscription(
input: { groupPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
) {
diff --git a/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql
index 317b48c142d..2e6bc8c36ba 100644
--- a/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql
@@ -1,4 +1,4 @@
-mutation updateEpic($input: UpdateEpicInput!) {
+mutation updateEpicTitle($input: UpdateEpicInput!) {
updateIssuableTitle: updateEpic(input: $input) {
epic {
title
diff --git a/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql
index 81891fb601f..e1a3927e7e1 100644
--- a/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql
@@ -1,4 +1,4 @@
-mutation issueSetSubscription($fullPath: ID!, $iid: String!, $subscribedState: Boolean!) {
+mutation projectIssueSetSubscription($fullPath: ID!, $iid: String!, $subscribedState: Boolean!) {
updateIssuableSubscription: issueSetSubscription(
input: { projectPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
) {
diff --git a/app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql
index 3c09daad793..016c31ea096 100644
--- a/app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql
@@ -2,6 +2,7 @@ mutation mergeRequestSetLabels($input: MergeRequestSetLabelsInput!) {
mergeRequestSetLabels(input: $input) {
errors
mergeRequest {
+ id
labels {
nodes {
color
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index cea26acd101..d8ab8f1c65b 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -5,6 +5,7 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
import sidebarDetailsMRQuery from '../queries/sidebarDetailsMR.query.graphql';
+import toggleAttentionRequestedMutation from '../queries/toggle_attention_requested.mutation.graphql';
const queries = {
merge_request: sidebarDetailsMRQuery,
@@ -90,4 +91,15 @@ export default class SidebarService {
},
});
}
+
+ toggleAttentionRequested(userId) {
+ return gqClient.mutate({
+ mutation: toggleAttentionRequestedMutation,
+ variables: {
+ userId: convertToGraphQLId(TYPE_USER, `${userId}`),
+ projectPath: this.fullPath,
+ iid: this.iid.toString(),
+ },
+ });
+ }
}
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 9144e3b08db..86580744ccc 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,6 +1,6 @@
import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
import createFlash from '~/flash';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import { visitUrl } from '../lib/utils/url_utility';
import Service from './services/sidebar_service';
@@ -56,13 +56,52 @@ export default class SidebarMediator {
return this.service
.requestReview(userId)
.then(() => {
- this.store.updateReviewer(userId);
+ this.store.updateReviewer(userId, 'reviewed');
toast(__('Requested review'));
callback(userId, true);
})
.catch(() => callback(userId, false));
}
+ async toggleAttentionRequested(type, { user, callback }) {
+ try {
+ const isReviewer = type === 'reviewer';
+ const reviewerOrAssignee = isReviewer
+ ? this.store.findReviewer(user)
+ : this.store.findAssignee(user);
+
+ await this.service.toggleAttentionRequested(user.id);
+
+ if (reviewerOrAssignee.attention_requested) {
+ toast(
+ sprintf(__('Removed attention request from @%{username}'), {
+ username: user.username,
+ }),
+ );
+ } else {
+ toast(sprintf(__('Requested attention from @%{username}'), { username: user.username }));
+ }
+
+ this.store.updateReviewer(user.id, 'attention_requested');
+ this.store.updateAssignee(user.id, 'attention_requested');
+
+ callback();
+ } catch (error) {
+ callback();
+ createFlash({
+ message: sprintf(__('Updating the attention request for %{username} failed.'), {
+ username: user.username,
+ }),
+ error,
+ captureError: true,
+ actionConfig: {
+ title: __('Try again'),
+ clickHandler: () => this.toggleAttentionRequired(type, { user, callback }),
+ },
+ });
+ }
+ }
+
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 94c54fc0980..5376791469e 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -82,11 +82,19 @@ export default class SidebarStore {
}
}
- updateReviewer(id) {
+ updateAssignee(id, stateKey) {
+ const assignee = this.findAssignee({ id });
+
+ if (assignee) {
+ assignee[stateKey] = !assignee[stateKey];
+ }
+ }
+
+ updateReviewer(id, stateKey) {
const reviewer = this.findReviewer({ id });
if (reviewer) {
- reviewer.reviewed = false;
+ reviewer[stateKey] = !reviewer[stateKey];
}
}
diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js
index 8e7368ef804..21f38c4d8c9 100644
--- a/app/assets/javascripts/snippets/index.js
+++ b/app/assets/javascripts/snippets/index.js
@@ -18,7 +18,6 @@ export default function appFactory(el, Component) {
{},
{
batchMax: 1,
- assumeImmutableResults: true,
},
),
});
diff --git a/app/assets/javascripts/static_site_editor/graphql/index.js b/app/assets/javascripts/static_site_editor/graphql/index.js
index 2ae2baddbcc..53572e680e5 100644
--- a/app/assets/javascripts/static_site_editor/graphql/index.js
+++ b/app/assets/javascripts/static_site_editor/graphql/index.js
@@ -22,7 +22,6 @@ const createApolloProvider = (appData) => {
},
{
typeDefs,
- assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/static_site_editor/pages/success.vue b/app/assets/javascripts/static_site_editor/pages/success.vue
index 70e692a0c86..eb03aa3cca3 100644
--- a/app/assets/javascripts/static_site_editor/pages/success.vue
+++ b/app/assets/javascripts/static_site_editor/pages/success.vue
@@ -30,7 +30,7 @@ export default {
updatedFileDescription() {
const { sourcePath } = this.appData;
- return sprintf(s__('Update %{sourcePath} file'), { sourcePath });
+ return sprintf(__('Update %{sourcePath} file'), { sourcePath });
},
},
created() {
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index 93353b400e5..79a30340856 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -12,6 +12,7 @@ export default class TaskList {
this.lockVersion = options.lockVersion;
this.taskListContainerSelector = `${this.selector} .js-task-list-container`;
this.updateHandler = this.update.bind(this);
+ this.onUpdate = options.onUpdate || (() => {});
this.onSuccess = options.onSuccess || (() => {});
this.onError =
options.onError ||
@@ -96,6 +97,7 @@ export default class TaskList {
},
};
+ this.onUpdate();
this.disableTaskListItems(e);
return axios
diff --git a/app/assets/javascripts/terms/components/app.vue b/app/assets/javascripts/terms/components/app.vue
new file mode 100644
index 00000000000..aedf5b6acfe
--- /dev/null
+++ b/app/assets/javascripts/terms/components/app.vue
@@ -0,0 +1,117 @@
+<script>
+import $ from 'jquery';
+import { GlButton, GlIntersectionObserver, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+
+import { FLASH_TYPES, FLASH_CLOSED_EVENT } from '~/flash';
+import { isLoggedIn } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import csrf from '~/lib/utils/csrf';
+import '~/behaviors/markdown/render_gfm';
+
+export default {
+ name: 'TermsApp',
+ i18n: {
+ accept: __('Accept terms'),
+ continue: __('Continue'),
+ decline: __('Decline and sign out'),
+ },
+ flashElements: [],
+ csrf,
+ directives: {
+ SafeHtml,
+ },
+ components: { GlButton, GlIntersectionObserver },
+ inject: ['terms', 'permissions', 'paths'],
+ data() {
+ return {
+ acceptDisabled: true,
+ };
+ },
+ computed: {
+ isLoggedIn,
+ },
+ mounted() {
+ this.renderGFM();
+ this.setScrollableViewportHeight();
+
+ this.$options.flashElements = [
+ ...document.querySelectorAll(
+ Object.values(FLASH_TYPES)
+ .map((flashType) => `.flash-${flashType}`)
+ .join(','),
+ ),
+ ];
+
+ this.$options.flashElements.forEach((flashElement) => {
+ flashElement.addEventListener(FLASH_CLOSED_EVENT, this.handleFlashClose);
+ });
+ },
+ beforeDestroy() {
+ this.$options.flashElements.forEach((flashElement) => {
+ flashElement.removeEventListener(FLASH_CLOSED_EVENT, this.handleFlashClose);
+ });
+ },
+ methods: {
+ renderGFM() {
+ $(this.$refs.gfmContainer).renderGFM();
+ },
+ handleBottomReached() {
+ this.acceptDisabled = false;
+ },
+ setScrollableViewportHeight() {
+ // Reset `max-height` inline style
+ this.$refs.scrollableViewport.style.maxHeight = '';
+
+ const { scrollHeight, clientHeight } = document.documentElement;
+
+ // Set `max-height` to 100vh minus all elements that are NOT the scrollable viewport (header, footer, alerts, etc)
+ this.$refs.scrollableViewport.style.maxHeight = `calc(100vh - ${
+ scrollHeight - clientHeight
+ }px)`;
+ },
+ handleFlashClose(event) {
+ this.setScrollableViewportHeight();
+ event.target.removeEventListener(FLASH_CLOSED_EVENT, this.handleFlashClose);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-card-body gl-relative gl-pb-0 gl-px-0" data-qa-selector="terms_content">
+ <div
+ class="terms-fade gl-absolute gl-left-5 gl-right-5 gl-bottom-0 gl-h-11 gl-pointer-events-none"
+ ></div>
+ <div
+ ref="scrollableViewport"
+ data-testid="scrollable-viewport"
+ class="gl-h-100vh gl-overflow-y-auto gl-pb-11 gl-px-5"
+ >
+ <div ref="gfmContainer" v-safe-html="terms"></div>
+ <gl-intersection-observer @appear="handleBottomReached">
+ <div></div>
+ </gl-intersection-observer>
+ </div>
+ </div>
+ <div v-if="isLoggedIn" class="gl-card-footer gl-display-flex gl-justify-content-end">
+ <form v-if="permissions.canDecline" method="post" :action="paths.decline">
+ <gl-button type="submit">{{ $options.i18n.decline }}</gl-button>
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ </form>
+ <form v-if="permissions.canAccept" class="gl-ml-3" method="post" :action="paths.accept">
+ <gl-button
+ type="submit"
+ variant="confirm"
+ :disabled="acceptDisabled"
+ data-qa-selector="accept_terms_button"
+ >{{ $options.i18n.accept }}</gl-button
+ >
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ </form>
+ <gl-button v-else class="gl-ml-3" :href="paths.root" variant="confirm">{{
+ $options.i18n.continue
+ }}</gl-button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/terms/index.js b/app/assets/javascripts/terms/index.js
new file mode 100644
index 00000000000..9d60fdfb50a
--- /dev/null
+++ b/app/assets/javascripts/terms/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+
+import TermsApp from 'jh_else_ce/terms/components/app.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+export const initTermsApp = () => {
+ const el = document.getElementById('js-terms-of-service');
+
+ if (!el) return false;
+
+ const { terms, permissions, paths } = convertObjectPropsToCamelCase(
+ JSON.parse(el.dataset.termsData),
+ { deep: true },
+ );
+
+ return new Vue({
+ el,
+ provide: { terms, permissions, paths },
+ render(createElement) {
+ return createElement(TermsApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/token_access/index.js b/app/assets/javascripts/token_access/index.js
index 8d29a65d705..6a29883290a 100644
--- a/app/assets/javascripts/token_access/index.js
+++ b/app/assets/javascripts/token_access/index.js
@@ -6,7 +6,7 @@ import TokenAccess from './components/token_access.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
export const initTokenAccess = (containerId = 'js-ci-token-access-app') => {
diff --git a/app/assets/javascripts/user_lists/components/user_lists_table.vue b/app/assets/javascripts/user_lists/components/user_lists_table.vue
index 765f59228a6..ccc2bfabb56 100644
--- a/app/assets/javascripts/user_lists/components/user_lists_table.vue
+++ b/app/assets/javascripts/user_lists/components/user_lists_table.vue
@@ -23,7 +23,7 @@ export default {
translations: {
createdTimeagoLabel: s__('UserList|created %{timeago}'),
deleteListTitle: s__('UserList|Delete %{name}?'),
- deleteListMessage: s__('User list %{name} will be removed. Are you sure?'),
+ deleteListMessage: __('User list %{name} will be removed. Are you sure?'),
editUserListLabel: s__('FeatureFlags|Edit User List'),
},
modal: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
new file mode 100644
index 00000000000..492e68b636f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
@@ -0,0 +1,77 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { escape } from 'lodash';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { n__, s__ } from '~/locale';
+
+const mergeCommitCount = s__('mrWidgetCommitsAdded|1 merge commit');
+
+export default {
+ components: {
+ GlSprintf,
+ },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ isSquashEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isFastForwardEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ commitsCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ targetBranch: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ targetBranchEscaped() {
+ return escape(this.targetBranch);
+ },
+ commitsCountMessage() {
+ return n__('%d commit', '%d commits', this.isSquashEnabled ? 1 : this.commitsCount);
+ },
+ message() {
+ return this.isFastForwardEnabled
+ ? s__('mrWidgetCommitsAdded|Adds %{commitCount} to %{targetBranch}.')
+ : s__(
+ 'mrWidgetCommitsAdded|Adds %{commitCount} and %{mergeCommitCount} to %{targetBranch}%{squashedCommits}.',
+ );
+ },
+ textDecorativeComponent() {
+ return this.glFeatures.restructuredMrWidget ? 'span' : 'strong';
+ },
+ },
+ mergeCommitCount,
+};
+</script>
+
+<template>
+ <span>
+ <gl-sprintf :message="message">
+ <template #commitCount>
+ <component :is="textDecorativeComponent" class="commits-count-message">{{
+ commitsCountMessage
+ }}</component>
+ </template>
+ <template #mergeCommitCount>
+ <component :is="textDecorativeComponent">{{ $options.mergeCommitCount }}</component>
+ </template>
+ <template #targetBranch>
+ <span class="label-branch">{{ targetBranchEscaped }}</span>
+ </template>
+ <template #squashedCommits>
+ <template v-if="glFeatures.restructuredMrWidget && isSquashEnabled">
+ {{ n__('(squashes %d commit)', '(squashes %d commits)', commitsCount) }}</template
+ ></template
+ >
+ </gl-sprintf>
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
index 0c4a5ee35d9..25dbb614c1d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
@@ -1,7 +1,11 @@
<script>
import { toNounSeriesText } from '~/lib/utils/grammar';
import { n__, sprintf } from '~/locale';
-import { APPROVED_MESSAGE } from '~/vue_merge_request_widget/components/approvals/messages';
+import {
+ APPROVED_BY_YOU_AND_OTHERS,
+ APPROVED_BY_YOU,
+ APPROVED_BY_OTHERS,
+} from '~/vue_merge_request_widget/components/approvals/messages';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
export default {
@@ -29,12 +33,23 @@ export default {
},
},
computed: {
- message() {
- if (this.approved) {
- return APPROVED_MESSAGE;
+ approvalLeftMessage() {
+ if (this.rulesLeft.length) {
+ return sprintf(
+ n__(
+ 'Requires %{count} approval from %{names}.',
+ 'Requires %{count} approvals from %{names}.',
+ this.approvalsLeft,
+ ),
+ {
+ names: toNounSeriesText(this.rulesLeft),
+ count: this.approvalsLeft,
+ },
+ false,
+ );
}
- if (!this.rulesLeft.length) {
+ if (!this.approved) {
return n__(
'Requires %d approval from eligible users.',
'Requires %d approvals from eligible users.',
@@ -42,32 +57,51 @@ export default {
);
}
- return sprintf(
- n__(
- 'Requires %{count} approval from %{names}.',
- 'Requires %{count} approvals from %{names}.',
- this.approvalsLeft,
- ),
- {
- names: toNounSeriesText(this.rulesLeft),
- count: this.approvalsLeft,
- },
- false,
- );
+ return '';
+ },
+ message() {
+ if (this.approvedByMe && this.approvedByOthers) {
+ return APPROVED_BY_YOU_AND_OTHERS;
+ }
+
+ if (this.approvedByMe) {
+ return APPROVED_BY_YOU;
+ }
+
+ if (this.approved) {
+ return APPROVED_BY_OTHERS;
+ }
+
+ return '';
},
hasApprovers() {
return Boolean(this.approvers.length);
},
+ approvedByMe() {
+ if (!this.currentUserId) {
+ return false;
+ }
+ return this.approvers.some((approver) => approver.id === this.currentUserId);
+ },
+ approvedByOthers() {
+ if (!this.currentUserId) {
+ return false;
+ }
+ return this.approvers.some((approver) => approver.id !== this.currentUserId);
+ },
+ currentUserId() {
+ return gon.current_user_id;
+ },
},
- APPROVED_MESSAGE,
};
</script>
<template>
<div data-qa-selector="approvals_summary_content">
- <strong>{{ message }}</strong>
+ <strong>{{ approvalLeftMessage }}</strong>
<template v-if="hasApprovers">
- <span>{{ s__('mrWidget|Approved by') }}</span>
+ <span v-if="approvalLeftMessage">{{ message }}</span>
+ <strong v-else>{{ message }}</strong>
<user-avatar-list class="d-inline-block align-middle" :items="approvers" />
</template>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js b/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js
index 0538c38307b..fbdefa95630 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js
@@ -6,4 +6,6 @@ export const FETCH_ERROR = s__(
);
export const APPROVE_ERROR = s__('mrWidget|An error occurred while submitting your approval.');
export const UNAPPROVE_ERROR = s__('mrWidget|An error occurred while removing your approval.');
-export const APPROVED_MESSAGE = s__('mrWidget|Merge request approved.');
+export const APPROVED_BY_YOU_AND_OTHERS = s__('mrWidget|Approved by you and others');
+export const APPROVED_BY_YOU = s__('mrWidget|Approved by you');
+export const APPROVED_BY_OTHERS = s__('mrWidget|Approved by');
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
index cbace1ad57c..f4f611dfd1b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
@@ -12,13 +12,12 @@ import {
CANCELED,
SKIPPED,
} from './constants';
-import MemoryUsage from './memory_usage.vue';
export default {
name: 'DeploymentInfo',
components: {
GlLink,
- MemoryUsage,
+ MemoryUsage: () => import('./memory_usage.vue'),
TooltipOnTruncate,
},
directives: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue
index d3384903cce..655acf28253 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue
@@ -2,10 +2,11 @@
import { GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import MrCollapsibleExtension from '../mr_collapsible_extension.vue';
+import Deployment from './deployment.vue';
export default {
components: {
- Deployment: () => import('./deployment.vue'),
+ Deployment,
GlSprintf,
MrCollapsibleExtension,
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
index 023367a794e..33a83aef057 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
@@ -24,13 +24,20 @@ export default {
return sprintf(__('%{widget} options'), { widget: this.widget });
},
},
+ methods: {
+ onClickAction(action) {
+ if (action.onClick) {
+ action.onClick();
+ }
+ },
+ },
};
</script>
<template>
<div>
<gl-dropdown
- v-if="tertiaryButtons"
+ v-if="tertiaryButtons.length"
:text="dropdownLabel"
icon="ellipsis_v"
no-caret
@@ -47,6 +54,7 @@ export default {
:key="index"
:href="btn.href"
:target="btn.target"
+ @click="onClickAction(btn)"
>
{{ btn.text }}
</gl-dropdown-item>
@@ -57,11 +65,12 @@ export default {
:key="index"
:href="btn.href"
:target="btn.target"
- :class="{ 'gl-mr-3': index > 1 }"
+ :class="{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }"
category="tertiary"
variant="confirm"
size="small"
- class="gl-display-none gl-md-display-block"
+ class="gl-display-none gl-md-display-block gl-float-left"
+ @click="onClickAction(btn)"
>
{{ btn.text }}
</gl-button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 298f7c7ad8c..6f10f788952 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -8,6 +8,8 @@ import {
GlTooltipDirective,
GlIntersectionObserver,
} from '@gitlab/ui';
+import { once } from 'lodash';
+import api from '~/api';
import { sprintf, s__, __ } from '~/locale';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import { EXTENSION_ICON_CLASS } from '../../constants';
@@ -102,8 +104,15 @@ export default {
});
},
methods: {
+ triggerRedisTracking: once(function triggerRedisTracking() {
+ if (this.$options.expandEvent) {
+ api.trackRedisHllUserEvent(this.$options.expandEvent);
+ }
+ }),
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
+
+ this.triggerRedisTracking();
},
loadAllData() {
if (this.fullData) return;
@@ -143,7 +152,10 @@ export default {
:is-loading="isLoadingSummary"
:icon-name="statusIconName"
/>
- <div class="media-body gl-display-flex gl-flex-direction-row!">
+ <div
+ class="media-body gl-display-flex gl-flex-direction-row!"
+ data-testid="widget-extension-top-level"
+ >
<div class="gl-flex-grow-1">
<template v-if="isLoadingSummary">{{ widgetLoadingText }}</template>
<div v-else v-safe-html="summary(collapsedData)"></div>
@@ -194,20 +206,28 @@ export default {
class="gl-display-flex gl-align-items-center gl-py-3 gl-pl-7"
data-testid="extension-list-item"
>
- <status-icon v-if="data.icon" :icon-name="data.icon.name" :size="12" />
+ <status-icon v-if="data.icon" :icon-name="data.icon.name" :size="12" class="gl-pl-0" />
<gl-intersection-observer
:options="{ rootMargin: '100px', thresholds: 0.1 }"
- class="gl-flex-wrap gl-align-self-center gl-display-flex"
+ class="gl-flex-wrap gl-display-flex gl-w-full"
@appear="appear(index)"
@disappear="disappear(index)"
>
- <div v-safe-html="data.text" class="gl-mr-4"></div>
+ <div
+ v-safe-html="data.text"
+ class="gl-mr-4 gl-display-flex gl-align-items-center"
+ ></div>
<div v-if="data.link">
<gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
</div>
<gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'">
{{ data.badge.text }}
</gl-badge>
+ <actions
+ :widget="$options.label || $options.name"
+ :tertiary-buttons="data.actions"
+ class="gl-ml-auto"
+ />
</gl-intersection-observer>
</li>
</smart-virtual-list>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
index 4ca0b660696..ec6e6ed2620 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
@@ -12,6 +12,7 @@ export const registerExtension = (extension) => {
name: extension.name,
props: extension.props,
i18n: extension.i18n,
+ expandEvent: extension.expandEvent,
computed: {
...Object.keys(extension.computed).reduce(
(acc, computedKey) => ({
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 5c67b9c7ab5..9070cb1fe65 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -151,7 +151,7 @@ export default {
right
data-qa-selector="download_dropdown"
>
- <gl-dropdown-section-header>{{ s__('Download as') }}</gl-dropdown-section-header>
+ <gl-dropdown-section-header>{{ __('Download as') }}</gl-dropdown-section-header>
<gl-dropdown-item
:href="mr.emailPatchesPath"
class="js-download-email-patches"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 9bb955c534f..f7c952f9ef6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -101,6 +101,9 @@ export default {
? this.pipeline.details.status
: {};
},
+ artifacts() {
+ return this.pipeline?.details?.artifacts;
+ },
hasStages() {
return this.pipeline?.details?.stages?.length > 0;
},
@@ -285,7 +288,7 @@ export default {
/>
</span>
<linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" />
- <pipeline-artifacts :pipeline-id="pipeline.id" class="gl-ml-3" />
+ <pipeline-artifacts :pipeline-id="pipeline.id" :artifacts="artifacts" class="gl-ml-3" />
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
index 306026072a3..c314261d3f5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
@@ -1,8 +1,10 @@
<script>
import { s__, n__ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'MRWidgetRelatedLinks',
+ mixins: [glFeatureFlagMixin()],
props: {
relatedLinks: {
type: Object,
@@ -14,6 +16,11 @@ export default {
required: false,
default: '',
},
+ showAssignToMe: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
closesText() {
@@ -30,16 +37,25 @@ export default {
};
</script>
<template>
- <section class="mr-info-list gl-ml-7 gl-pb-5">
- <p v-if="relatedLinks.closing">
+ <section>
+ <p
+ v-if="relatedLinks.closing"
+ :class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
+ >
{{ closesText }}
<span v-html="relatedLinks.closing /* eslint-disable-line vue/no-v-html */"></span>
</p>
- <p v-if="relatedLinks.mentioned">
+ <p
+ v-if="relatedLinks.mentioned"
+ :class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
+ >
{{ n__('mrWidget|Mentions issue', 'mrWidget|Mentions issues', relatedLinks.mentionedCount) }}
<span v-html="relatedLinks.mentioned /* eslint-disable-line vue/no-v-html */"></span>
</p>
- <p v-if="relatedLinks.assignToMe">
+ <p
+ v-if="relatedLinks.assignToMe && showAssignToMe"
+ :class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
+ >
<span v-html="relatedLinks.assignToMe /* eslint-disable-line vue/no-v-html */"></span>
</p>
</section>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index f3673005c45..cd5b7c3110d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -4,9 +4,7 @@ import Tracking from '~/tracking';
import DismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
import {
SP_TRACK_LABEL,
- SP_LINK_TRACK_EVENT,
SP_SHOW_TRACK_EVENT,
- SP_LINK_TRACK_VALUE,
SP_SHOW_TRACK_VALUE,
SP_HELP_CONTENT,
SP_HELP_URL,
@@ -20,9 +18,7 @@ export default {
name: 'MRWidgetSuggestPipeline',
SP_ICON_NAME,
SP_TRACK_LABEL,
- SP_LINK_TRACK_EVENT,
SP_SHOW_TRACK_EVENT,
- SP_LINK_TRACK_VALUE,
SP_SHOW_TRACK_VALUE,
SP_HELP_CONTENT,
SP_HELP_URL,
@@ -81,29 +77,14 @@ export default {
<div>
<gl-sprintf
:message="
- s__(`mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd}
- %{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd}
- to create one.`)
+ s__(`mrWidget|%{boldHeaderStart}Looks like there's no pipeline here.%{boldHeaderEnd}`)
"
>
- <template #prefixToLink="{ content }">
+ <template #boldHeader="{ content }">
<strong>
{{ content }}
</strong>
</template>
- <template #addPipelineLink="{ content }">
- <gl-link
- :href="pipelinePath"
- class="gl-ml-1"
- data-testid="add-pipeline-link"
- :data-track-property="humanAccess"
- :data-track-value="$options.SP_LINK_TRACK_VALUE"
- :data-track-action="$options.SP_LINK_TRACK_EVENT"
- :data-track-label="$options.SP_TRACK_LABEL"
- >
- {{ content }}
- </gl-link>
- </template>
</gl-sprintf>
</div>
</template>
@@ -115,9 +96,6 @@ export default {
</div>
<div class="col-md-7 order-md-first col-12">
<div class="ml-6 gl-pt-5">
- <strong>
- {{ s__('mrWidget|Are you adding technical debt or code vulnerabilities?') }}
- </strong>
<p class="gl-mt-2">
<gl-sprintf :message="$options.SP_HELP_CONTENT">
<template #link="{ content }">
@@ -142,7 +120,7 @@ export default {
:data-track-action="$options.SP_SHOW_TRACK_EVENT"
:data-track-label="$options.SP_TRACK_LABEL"
>
- {{ __('Show me how to add a pipeline') }}
+ {{ __('Try out GitLab Pipelines') }}
</gl-button>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
index 9268e426954..caafd6b995e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
@@ -4,7 +4,7 @@ import { __ } from '../../locale';
export default {
i18n: {
- removesBranchText: __('The source branch will be deleted'),
+ removesBranchText: __('Deletes the source branch'),
tooltipTitle: __('A user with write access to the source branch selected this option'),
},
components: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
index 44bdc4a3be8..3eda2828e97 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
@@ -1,5 +1,8 @@
<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
export default {
+ mixins: [glFeatureFlagMixin()],
props: {
value: {
type: String,
@@ -20,7 +23,10 @@ export default {
<template>
<li>
<div class="commit-message-editor">
- <div class="d-flex flex-wrap align-items-center justify-content-between">
+ <div
+ :class="{ 'gl-mb-3': glFeatures.restructuredMrWidget }"
+ class="d-flex flex-wrap align-items-center justify-content-between"
+ >
<label class="col-form-label" :for="inputId">
<strong>{{ label }}</strong>
</label>
@@ -35,7 +41,7 @@ export default {
rows="7"
@input="$emit('input', $event.target.value)"
></textarea>
- <slot name="checkbox"></slot>
+ <slot name="text-muted"></slot>
</div>
</li>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
index 3ca193514f1..5c4a526bcc3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
@@ -1,15 +1,12 @@
<script>
-import { GlButton, GlSprintf } from '@gitlab/ui';
-import { escape } from 'lodash';
-import { __, n__, s__ } from '~/locale';
-
-const mergeCommitCount = s__('mrWidgetCommitsAdded|1 merge commit');
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import AddedCommitMessage from '../added_commit_message.vue';
export default {
- mergeCommitCount,
components: {
GlButton,
- GlSprintf,
+ AddedCommitMessage,
},
props: {
isSquashEnabled: {
@@ -39,9 +36,6 @@ export default {
collapseIcon() {
return this.expanded ? 'chevron-down' : 'chevron-right';
},
- commitsCountMessage() {
- return n__('%d commit', '%d commits', this.isSquashEnabled ? 1 : this.commitsCount);
- },
modifyLinkMessage() {
if (this.isFastForwardEnabled) return __('Modify commit message');
else if (this.isSquashEnabled) return __('Modify commit messages');
@@ -50,16 +44,6 @@ export default {
ariaLabel() {
return this.expanded ? __('Collapse') : __('Expand');
},
- targetBranchEscaped() {
- return escape(this.targetBranch);
- },
- message() {
- return this.isFastForwardEnabled
- ? s__('mrWidgetCommitsAdded|%{commitCount} will be added to %{targetBranch}.')
- : s__(
- 'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}.',
- );
- },
},
methods: {
toggle() {
@@ -86,17 +70,12 @@ export default {
<span v-if="expanded">{{ __('Collapse') }}</span>
<span v-else>
<span class="vertical-align-middle">
- <gl-sprintf :message="message">
- <template #commitCount>
- <strong class="commits-count-message">{{ commitsCountMessage }}</strong>
- </template>
- <template #mergeCommitCount>
- <strong>{{ $options.mergeCommitCount }}</strong>
- </template>
- <template #targetBranch>
- <span class="label-branch">{{ targetBranchEscaped }}</span>
- </template>
- </gl-sprintf>
+ <added-commit-message
+ :is-squash-enabled="isSquashEnabled"
+ :is-fast-forward-enabled="isFastForwardEnabled"
+ :commits-count="commitsCount"
+ :target-branch="targetBranch"
+ />
</span>
<gl-button variant="link" class="modify-message-button">
{{ modifyLinkMessage }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 0eb173edbcb..a44caf886a4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -177,10 +177,10 @@ export default {
</h4>
<section class="mr-info-list">
<p v-if="shouldRemoveSourceBranch">
- {{ s__('mrWidget|The source branch will be deleted') }}
+ {{ s__('mrWidget|Deletes the source branch') }}
</p>
<p v-else class="gl-display-flex">
- <span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
+ <span class="gl-mr-3">{{ s__('mrWidget|Does not delete the source branch') }}</span>
<gl-button
v-if="canRemoveSourceBranch"
:loading="isRemovingSourceBranch"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
index e02be6dc2f7..10b93d7849f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
@@ -1,4 +1,5 @@
<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
@@ -6,11 +7,12 @@ export default {
components: {
statusIcon,
},
+ mixins: [glFeatureFlagMixin()],
};
</script>
<template>
<div class="mr-widget-body media">
- <status-icon :show-disabled-button="true" status="loading" />
+ <status-icon :show-disabled-button="!glFeatures.restructuredMrWidget" status="loading" />
<div class="media-body space-children">
<span class="bold"> {{ s__('mrWidget|Checking if merge request can be merged…') }} </span>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index a1759b1a815..84dac95ce74 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -1,6 +1,7 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlLoadingIcon, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import api from '~/api';
import createFlash from '~/flash';
import { s__, __ } from '~/locale';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
@@ -83,6 +84,8 @@ export default {
removeSourceBranch() {
this.isMakingRequest = true;
+ api.trackRedisHllUserEvent('i_code_review_post_merge_delete_branch');
+
this.service
.removeSourceBranch()
.then((res) => res.data)
@@ -103,9 +106,13 @@ export default {
});
},
openRevertModal() {
+ api.trackRedisHllUserEvent('i_code_review_post_merge_click_revert');
+
modalEventHub.$emit(OPEN_REVERT_MODAL);
},
openCherryPickModal() {
+ api.trackRedisHllUserEvent('i_code_review_post_merge_click_cherry_pick');
+
modalEventHub.$emit(OPEN_CHERRY_PICK_MODAL);
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
index 1c245b584ea..247877a8235 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
@@ -32,7 +32,7 @@ export default {
</h4>
<section class="mr-info-list">
<p>
- {{ s__('mrWidget|The changes will be merged into') }}
+ {{ s__('mrWidget|Merges changes into') }}
<span class="label-branch">
<a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a>
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 1976d3639a6..9f2870d8d69 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -1,8 +1,7 @@
<script>
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
-import { escape } from 'lodash';
import createFlash from '~/flash';
-import { __, sprintf } from '~/locale';
+import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
@@ -85,13 +84,7 @@ export default {
return ['failed', 'loading'].includes(this.status);
},
fastForwardMergeText() {
- return sprintf(
- __('Merge blocked: the source branch must be rebased onto the target branch.'),
- {
- targetBranch: `<span class="label-branch">${escape(this.targetBranch)}</span>`,
- },
- false,
- );
+ return __('Merge blocked: the source branch must be rebased onto the target branch.');
},
},
methods: {
@@ -170,8 +163,8 @@ export default {
v-if="!rebaseInProgress && !canPushToSourceBranch"
class="gl-font-weight-bold gl-ml-0!"
data-testid="rebase-message"
- v-html="fastForwardMergeText /* eslint-disable-line vue/no-v-html */"
- ></span>
+ >{{ fastForwardMergeText }}</span
+ >
<div
v-if="!rebaseInProgress && canPushToSourceBranch && !isMakingRequest"
class="accept-merge-holder clearfix js-toggle-container accept-action media space-children"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
index 9a7743348ff..0b6aa104181 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
@@ -37,7 +37,7 @@ export default {
<template>
<div class="mr-widget-body media">
<status-icon status="success" />
- <p class="media-body gl-m-0! gl-font-weight-bold">
+ <p class="media-body gl-m-0! gl-font-weight-bold gl-text-gray-900!">
<template v-if="canMerge">
{{ __('Ready to merge!') }}
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 7827c79cd31..2d704d3b07a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlSprintf, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
+import api from '~/api';
import { helpPagePath } from '~/helpers/help_page_helper';
export default {
@@ -22,6 +23,11 @@ export default {
data() {
return { emptyStateSVG };
},
+ methods: {
+ onClickNewFile() {
+ api.trackRedisHllUserEvent('i_code_review_widget_nothing_merge_click_new_file');
+ },
+ },
ciHelpPage: helpPagePath('/ci/quick_start/index.html'),
safeHtmlConfig: { ADD_TAGS: ['use'] },
};
@@ -59,6 +65,7 @@ export default {
category="secondary"
variant="success"
data-testid="createFileButton"
+ @click="onClickNewFile"
>
{{ __('Create file') }}
</gl-button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 7d4bd4cf1bf..d2cc99302a9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -18,9 +18,10 @@ import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests
import createFlash from '~/flash';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import simplePoll from '~/lib/utils/simple_poll';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
import SmartInterval from '~/smart_interval';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { helpPagePath } from '~/helpers/help_page_helper';
import MergeRequest from '../../../merge_request';
import {
AUTO_MERGE_STRATEGIES,
@@ -35,6 +36,8 @@ import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import MergeRequestStore from '../../stores/mr_widget_store';
import statusIcon from '../mr_widget_status_icon.vue';
+import AddedCommitMessage from '../added_commit_message.vue';
+import RelatedLinks from '../mr_widget_related_links.vue';
import CommitEdit from './commit_edit.vue';
import CommitMessageDropdown from './commit_message_dropdown.vue';
import CommitsHeader from './commits_header.vue';
@@ -113,6 +116,8 @@ export default {
import(
'ee_component/vue_merge_request_widget/components/merge_train_failed_pipeline_confirmation_dialog.vue'
),
+ AddedCommitMessage,
+ RelatedLinks,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -134,6 +139,7 @@ export default {
isSquashReadOnly: this.mr.squashIsReadonly,
squashCommitMessage: this.mr.squashCommitMessage,
isPipelineFailedModalVisible: false,
+ editCommitMessage: false,
};
},
computed: {
@@ -162,7 +168,7 @@ export default {
},
isMergeAllowed() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.state.mergeable || false;
+ return this.state.mergeable;
}
return this.mr.isMergeAllowed;
@@ -174,6 +180,11 @@ export default {
return this.mr.canRemoveSourceBranch;
},
+ commitTemplateHelpPage() {
+ return helpPagePath('user/project/merge_requests/commit_templates.md', {
+ anchor: 'merge-commit-message-template',
+ });
+ },
commits() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.commitsWithoutMergeCommits.nodes;
@@ -279,6 +290,10 @@ export default {
return enableSquashBeforeMerge && this.commitsCount > 1;
},
shouldShowMergeControls() {
+ if (this.glFeatures.restructuredMrWidget) {
+ return this.restructuredWidgetShowMergeButtons;
+ }
+
return this.isMergeAllowed || this.isAutoMergeAvailable;
},
shouldShowSquashEdit() {
@@ -297,15 +312,26 @@ export default {
showDangerMessageForMergeTrain() {
return this.preferredAutoMergeStrategy === MT_MERGE_STRATEGY && this.isPipelineFailed;
},
+ restructuredWidgetShowMergeButtons() {
+ if (this.glFeatures.restructuredMrWidget) {
+ return this.isMergeAllowed && this.state.userPermissions.canMerge;
+ }
+
+ return true;
+ },
},
mounted() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
eventHub.$on('ApprovalUpdated', this.updateGraphqlState);
+ eventHub.$on('MRWidgetUpdateRequested', this.updateGraphqlState);
+ eventHub.$on('mr.discussion.updated', this.updateGraphqlState);
}
},
beforeDestroy() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
eventHub.$off('ApprovalUpdated', this.updateGraphqlState);
+ eventHub.$off('MRWidgetUpdateRequested', this.updateGraphqlState);
+ eventHub.$off('mr.discussion.updated', this.updateGraphqlState);
}
if (this.pollingInterval) {
@@ -327,15 +353,6 @@ export default {
updateGraphqlState() {
return this.$apollo.queries.state.refetch();
},
- updateMergeCommitMessage(includeDescription) {
- const commitMessage = this.glFeatures.mergeRequestWidgetGraphql
- ? this.state.defaultMergeCommitMessage
- : this.mr.commitMessage;
- const commitMessageWithDescription = this.glFeatures.mergeRequestWidgetGraphql
- ? this.state.defaultMergeCommitMessageWithDescription
- : this.mr.commitMessageWithDescription;
- this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
- },
handleMergeButtonClick(useAutoMerge, mergeImmediately = false, confirmationClicked = false) {
if (this.showFailedPipelineModal && !confirmationClicked) {
this.isPipelineFailedModalVisible = true;
@@ -488,11 +505,21 @@ export default {
});
},
},
+ i18n: {
+ mergeCommitTemplateHintText: s__(
+ 'mrWidget|To change this default message, edit the template for merge commit messages. %{linkStart}Learn more.%{linkEnd}',
+ ),
+ },
};
</script>
<template>
- <div>
+ <div
+ :class="{
+ 'gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7':
+ glFeatures.restructuredMrWidget,
+ }"
+ >
<div v-if="loading" class="mr-widget-body">
<div class="gl-w-full mr-ready-to-merge-loader">
<gl-skeleton-loader :width="418" :height="30">
@@ -504,11 +531,16 @@ export default {
</div>
</div>
<template v-else>
- <div class="mr-widget-body media">
- <status-icon :status="iconClass" />
+ <div
+ class="mr-widget-body media"
+ :class="{
+ 'mr-widget-body-line-height-1': glFeatures.restructuredMrWidget,
+ }"
+ >
+ <status-icon v-if="!glFeatures.restructuredMrWidget" :status="iconClass" />
<div class="media-body">
- <div class="mr-widget-body-controls gl-display-flex gl-align-items-center">
- <gl-button-group class="gl-align-self-start">
+ <div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap">
+ <gl-button-group v-if="restructuredWidgetShowMergeButtons" class="gl-align-self-start">
<gl-button
size="medium"
category="primary"
@@ -555,14 +587,27 @@ export default {
</gl-button-group>
<div
v-if="shouldShowMergeControls"
+ :class="{ 'gl-w-full gl-order-n1 gl-mb-5': glFeatures.restructuredMrWidget }"
class="gl-display-flex gl-align-items-center gl-flex-wrap"
>
+ <merge-train-helper-icon
+ v-if="shouldRenderMergeTrainHelperIcon"
+ :merge-train-when-pipeline-succeeds-docs-path="
+ mr.mergeTrainWhenPipelineSucceedsDocsPath
+ "
+ class="gl-mx-3"
+ />
+
<gl-form-checkbox
v-if="canRemoveSourceBranch"
id="remove-source-branch-input"
v-model="removeSourceBranch"
:disabled="isRemoveSourceBranchButtonDisabled"
- class="js-remove-source-branch-checkbox gl-mx-3 gl-display-flex gl-align-items-center"
+ :class="{
+ 'gl-mx-3': !glFeatures.restructuredMrWidget,
+ 'gl-mr-5': glFeatures.restructuredMrWidget,
+ }"
+ class="js-remove-source-branch-checkbox gl-display-flex gl-align-items-center"
>
{{ __('Delete source branch') }}
</gl-form-checkbox>
@@ -573,38 +618,146 @@ export default {
v-model="squashBeforeMerge"
:help-path="mr.squashBeforeMergeHelpPath"
:is-disabled="isSquashReadOnly"
- class="gl-mx-3"
+ :class="{
+ 'gl-mx-3': !glFeatures.restructuredMrWidget,
+ 'gl-mr-5': glFeatures.restructuredMrWidget,
+ }"
/>
- <merge-train-helper-icon
- v-if="shouldRenderMergeTrainHelperIcon"
- :merge-train-when-pipeline-succeeds-docs-path="
- mr.mergeTrainWhenPipelineSucceedsDocsPath
+ <gl-form-checkbox
+ v-if="
+ glFeatures.restructuredMrWidget && (shouldShowSquashEdit || shouldShowMergeEdit)
"
- />
+ v-model="editCommitMessage"
+ class="gl-display-flex gl-align-items-center"
+ >
+ {{ __('Edit commit message') }}
+ </gl-form-checkbox>
+ </div>
+ <div
+ v-else-if="!glFeatures.restructuredMrWidget"
+ class="bold js-resolve-mr-widget-items-message gl-ml-3"
+ >
+ <div
+ v-if="hasPipelineMustSucceedConflict"
+ class="gl-display-flex gl-align-items-center"
+ data-testid="pipeline-succeed-conflict"
+ >
+ <gl-sprintf :message="pipelineMustSucceedConflictText" />
+ <gl-link
+ :href="mr.pipelineMustSucceedDocsPath"
+ target="_blank"
+ class="gl-display-flex gl-ml-2"
+ >
+ <gl-icon name="question" />
+ </gl-link>
+ </div>
+ <gl-sprintf v-else :message="mergeDisabledText" />
</div>
- <template v-else>
- <div class="bold js-resolve-mr-widget-items-message gl-ml-3">
- <div
- v-if="hasPipelineMustSucceedConflict"
- class="gl-display-flex gl-align-items-center"
- data-testid="pipeline-succeed-conflict"
+ <template v-if="glFeatures.restructuredMrWidget">
+ <div v-show="editCommitMessage" class="gl-w-full gl-order-n1">
+ <ul
+ :class="{
+ 'content-list': !glFeatures.restructuredMrWidget,
+ 'gl-list-style-none gl-p-0 gl-pt-4': glFeatures.restructuredMrWidget,
+ }"
+ class="border-top commits-list flex-list"
>
- <gl-sprintf :message="pipelineMustSucceedConflictText" />
- <gl-link
- :href="mr.pipelineMustSucceedDocsPath"
- target="_blank"
- class="gl-display-flex gl-ml-2"
+ <commit-edit
+ v-if="shouldShowSquashEdit"
+ v-model="squashCommitMessage"
+ :label="__('Squash commit message')"
+ input-id="squash-message-edit"
+ class="gl-m-0! gl-p-0!"
+ >
+ <template #header>
+ <commit-message-dropdown v-model="squashCommitMessage" :commits="commits" />
+ </template>
+ </commit-edit>
+ <commit-edit
+ v-if="shouldShowMergeEdit"
+ v-model="commitMessage"
+ :label="__('Merge commit message')"
+ input-id="merge-message-edit"
+ class="gl-m-0! gl-p-0!"
>
- <gl-icon name="question" />
- </gl-link>
- </div>
- <gl-sprintf v-else :message="mergeDisabledText" />
+ <template #text-muted>
+ <p class="form-text text-muted">
+ <gl-sprintf :message="$options.i18n.mergeCommitTemplateHintText">
+ <template #link="{ content }">
+ <gl-link
+ :href="commitTemplateHelpPage"
+ class="inline-link"
+ target="_blank"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </commit-edit>
+ </ul>
+ </div>
+ <div
+ v-if="!restructuredWidgetShowMergeButtons"
+ class="gl-w-full gl-order-n1 gl-text-gray-500"
+ >
+ <strong>
+ {{ __('Merge details') }}
+ </strong>
+ <ul class="gl-pl-4 gl-m-0">
+ <li class="gl-line-height-normal">
+ <added-commit-message
+ :is-squash-enabled="squashBeforeMerge"
+ :is-fast-forward-enabled="!shouldShowMergeEdit"
+ :commits-count="commitsCount"
+ :target-branch="stateData.targetBranch"
+ />
+ </li>
+ <li class="gl-line-height-normal">
+ <template v-if="removeSourceBranch">
+ {{ __('Deletes the source branch.') }}
+ </template>
+ <template v-else>
+ {{ __('Does not delete the source branch.') }}
+ </template>
+ </li>
+ <li v-if="mr.relatedLinks" class="gl-line-height-normal">
+ <related-links
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ :show-assign-to-me="false"
+ class="mr-ready-merge-related-links gl-display-inline"
+ />
+ </li>
+ </ul>
+ </div>
+ <div
+ v-else
+ :class="{ 'gl-mb-5': restructuredWidgetShowMergeButtons }"
+ class="gl-w-full gl-order-n1 gl-text-gray-500"
+ >
+ <added-commit-message
+ :is-squash-enabled="squashBeforeMerge"
+ :is-fast-forward-enabled="!shouldShowMergeEdit"
+ :commits-count="commitsCount"
+ :target-branch="stateData.targetBranch"
+ />
+ <template v-if="mr.relatedLinks">
+ &middot;
+ <related-links
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ :show-assign-to-me="false"
+ class="mr-ready-merge-related-links gl-display-inline"
+ />
+ </template>
</div>
</template>
</div>
<div
- v-if="showDangerMessageForMergeTrain"
+ v-if="showDangerMessageForMergeTrain && !glFeatures.restructuredMrWidget"
class="gl-mt-5 gl-text-gray-500"
data-testid="failed-pipeline-merge-train-text"
>
@@ -612,7 +765,7 @@ export default {
</div>
</div>
</div>
- <template v-if="shouldShowMergeControls">
+ <template v-if="shouldShowMergeControls && !glFeatures.restructuredMrWidget">
<div
v-if="!shouldShowMergeEdit"
class="mr-fast-forward-message"
@@ -621,7 +774,7 @@ export default {
{{ __('Fast-forward merge without a merge commit') }}
</div>
<commits-header
- v-if="shouldShowSquashEdit || shouldShowMergeEdit"
+ v-if="!glFeatures.restructuredMrWidget && (shouldShowSquashEdit || shouldShowMergeEdit)"
:is-squash-enabled="squashBeforeMerge"
:commits-count="commitsCount"
:target-branch="stateData.targetBranch"
@@ -646,15 +799,16 @@ export default {
:label="__('Merge commit message')"
input-id="merge-message-edit"
>
- <template #checkbox>
- <label>
- <input
- id="include-description"
- type="checkbox"
- @change="updateMergeCommitMessage($event.target.checked)"
- />
- {{ __('Include merge request description') }}
- </label>
+ <template #text-muted>
+ <p class="form-text text-muted">
+ <gl-sprintf :message="$options.i18n.mergeCommitTemplateHintText">
+ <template #link="{ content }">
+ <gl-link :href="commitTemplateHelpPage" class="inline-link" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
</template>
</commit-edit>
</ul>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index 41b5983ae0c..c6227c4394d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -1,15 +1,18 @@
<script>
-import { GlIcon, GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective, GlFormCheckbox, GlLink } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { SQUASH_BEFORE_MERGE } from '../../i18n';
export default {
components: {
GlIcon,
GlFormCheckbox,
+ GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
i18n: {
...SQUASH_BEFORE_MERGE,
},
@@ -33,6 +36,9 @@ export default {
tooltipTitle() {
return this.isDisabled ? this.$options.i18n.tooltipTitle : null;
},
+ helpIconName() {
+ return this.glFeatures.restructuredMrWidget ? 'question-o' : 'question';
+ },
},
};
</script>
@@ -51,18 +57,18 @@ export default {
>
{{ $options.i18n.checkboxLabel }}
</gl-form-checkbox>
- <a
+ <gl-link
v-if="helpPath"
v-gl-tooltip
:href="helpPath"
:title="$options.i18n.helpLabel"
+ :class="{ 'gl-text-blue-600': glFeatures.restructuredMrWidget }"
target="_blank"
- rel="noopener noreferrer nofollow"
>
- <gl-icon name="question" />
+ <gl-icon :name="helpIconName" />
<span class="sr-only">
{{ $options.i18n.helpLabel }}
</span>
- </a>
+ </gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index 790870ee4c6..fa4f8b76cb9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -10,8 +10,8 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import getStateQuery from '../../queries/get_state.query.graphql';
-import workInProgressQuery from '../../queries/states/work_in_progress.query.graphql';
-import removeWipMutation from '../../queries/toggle_wip.mutation.graphql';
+import draftQuery from '../../queries/states/draft.query.graphql';
+import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql';
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
@@ -23,7 +23,7 @@ export default {
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
apollo: {
userPermissions: {
- query: workInProgressQuery,
+ query: draftQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
@@ -53,25 +53,25 @@ export default {
},
},
methods: {
- removeWipMutation() {
+ removeDraftMutation() {
const { mergeRequestQueryVariables } = this;
this.isMakingRequest = true;
this.$apollo
.mutate({
- mutation: removeWipMutation,
+ mutation: removeDraftMutation,
variables: {
...mergeRequestQueryVariables,
- wip: false,
+ draft: false,
},
update(
store,
{
data: {
- mergeRequestSetWip: {
+ mergeRequestSetDraft: {
errors,
- mergeRequest: { mergeableDiscussionsState, workInProgress, title },
+ mergeRequest: { mergeableDiscussionsState, draft, title },
},
},
},
@@ -91,7 +91,7 @@ export default {
const data = produce(sourceData, (draftState) => {
draftState.project.mergeRequest.mergeableDiscussionsState = mergeableDiscussionsState;
- draftState.project.mergeRequest.workInProgress = workInProgress;
+ draftState.project.mergeRequest.draft = draft;
draftState.project.mergeRequest.title = title;
});
@@ -104,14 +104,14 @@ export default {
optimisticResponse: {
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Mutation',
- mergeRequestSetWip: {
+ mergeRequestSetDraft: {
__typename: 'MergeRequestSetWipPayload',
errors: [],
mergeRequest: {
__typename: 'MergeRequest',
mergeableDiscussionsState: true,
title: this.mr.title,
- workInProgress: false,
+ draft: false,
},
},
},
@@ -119,7 +119,7 @@ export default {
.then(
({
data: {
- mergeRequestSetWip: {
+ mergeRequestSetDraft: {
mergeRequest: { title },
},
},
@@ -137,9 +137,9 @@ export default {
this.isMakingRequest = false;
});
},
- handleRemoveWIP() {
+ handleRemoveDraft() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
- this.removeWipMutation();
+ this.removeDraftMutation();
} else {
this.isMakingRequest = true;
this.service
@@ -178,8 +178,8 @@ export default {
size="small"
:disabled="isMakingRequest"
:loading="isMakingRequest"
- class="js-remove-wip gl-ml-3"
- @click="handleRemoveWIP"
+ class="js-remove-draft gl-ml-3"
+ @click="handleRemoveDraft"
>
{{ s__('mrWidget|Mark as ready') }}
</gl-button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index b88e83ccb0f..d0c6cf12e25 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -17,14 +17,12 @@ export const AUTO_MERGE_STRATEGIES = [MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY,
// SP - "Suggest Pipelines"
export const SP_TRACK_LABEL = 'no_pipeline_noticed';
-export const SP_LINK_TRACK_EVENT = 'click_link';
export const SP_SHOW_TRACK_EVENT = 'click_button';
-export const SP_LINK_TRACK_VALUE = 30;
export const SP_SHOW_TRACK_VALUE = 10;
export const SP_HELP_CONTENT = s__(
- `mrWidget|Use %{linkStart}CI pipelines to test your code%{linkEnd} by simply adding a GitLab CI configuration file to your project. It only takes a minute to make your code more secure and robust.`,
+ `mrWidget|GitLab %{linkStart}CI/CD can automatically build, test, and deploy your application.%{linkEnd} It only takes a few minutes to get started, and we can help you create a pipeline configuration file.`,
);
-export const SP_HELP_URL = 'https://about.gitlab.com/blog/2019/07/12/guide-to-ci-cd-pipelines/';
+export const SP_HELP_URL = 'https://docs.gitlab.com/ee/ci/quick_start/';
export const SP_ICON_NAME = 'status_notfound';
export const MERGE_ACTIVE_STATUS_PHRASES = [
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
index 349e9d29355..9cbc0b0e5d1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
@@ -11,6 +11,7 @@ export default {
label: 'Issues',
loading: 'Loading issues...',
},
+ expandEvent: 'i_testing_load_performance_widget_total',
// Add an array of props
// These then get mapped to values stored in the MR Widget store
props: ['targetProjectFullPath', 'conflictsDocsPath'],
@@ -29,7 +30,15 @@ export default {
// Tertiary action buttons that will take the user elsewhere
// in the GitLab app
tertiaryButtons() {
- return [{ text: 'Full report', href: this.conflictsDocsPath, target: '_blank' }];
+ return [
+ {
+ text: 'Click me',
+ onClick() {
+ console.log('Hello world');
+ },
+ },
+ { text: 'Full report', href: this.conflictsDocsPath, target: '_blank' },
+ ];
},
},
methods: {
@@ -66,6 +75,7 @@ export default {
// href: 'https://google.com', // Required: href for the link
// text: 'Link text', // Required: Text to be used inside the link
// },
+ actions: [{ text: 'Full report', href: 'https://gitlab.com', target: '_blank' }],
}));
});
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql b/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql
index 389a81e0a61..da1cace4598 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql
@@ -1,4 +1,4 @@
-query getIssues($projectPath: ID!) {
+query getProjectIssues($projectPath: ID!) {
project(fullPath: $projectPath) {
issues {
count
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index f5dbcec7dbe..8d596465970 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -13,12 +13,7 @@ Vue.use(Translate);
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
export default () => {
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index cf6472f2c8c..83789f10285 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -1,9 +1,13 @@
import { __ } from '~/locale';
export const MERGE_DISABLED_TEXT = __('You can only merge once the items above are resolved.');
+export const MERGE_DISABLED_SKIPPED_PIPELINE_TEXT = __(
+ "Merge blocked: pipeline must succeed. It's waiting for a manual job to continue.",
+);
export const PIPELINE_MUST_SUCCEED_CONFLICT_TEXT = __(
'A CI/CD pipeline must run and be successful before merge.',
);
+export const PIPELINE_SKIPPED_STATUS = 'SKIPPED';
export default {
computed: {
@@ -17,6 +21,10 @@ export default {
);
},
mergeDisabledText() {
+ if (this.pipeline?.status === PIPELINE_SKIPPED_STATUS) {
+ return MERGE_DISABLED_SKIPPED_PIPELINE_TEXT;
+ }
+
return MERGE_DISABLED_TEXT;
},
pipelineMustSucceedConflictText() {
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 3ac1e881658..c98dc426224 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
@@ -91,6 +91,7 @@ export default {
MrWidgetApprovals,
SecurityReportsApp: () => import('~/vue_shared/security_reports/security_reports_app.vue'),
MergeChecksFailed: () => import('./components/states/merge_checks_failed.vue'),
+ ReadyToMerge: ReadyToMergeState,
},
apollo: {
state: {
@@ -213,6 +214,9 @@ export default {
window.gon?.features?.refactorMrWidgetsExtensionsUser
);
},
+ isRestructuredMrWidgetEnabled() {
+ return window.gon?.features?.restructuredMrWidget;
+ },
},
watch: {
'mr.machineValue': {
@@ -547,12 +551,17 @@ export default {
<div class="mr-widget-section">
<component :is="componentName" :mr="mr" :service="service" />
-
- <div class="mr-widget-info">
+ <ready-to-merge
+ v-if="isRestructuredMrWidgetEnabled && mr.commitsCount"
+ :mr="mr"
+ :service="service"
+ />
+ <div v-else class="mr-widget-info">
<mr-widget-related-links
v-if="shouldRenderRelatedLinks"
:state="mr.state"
:related-links="mr.relatedLinks"
+ class="mr-info-list gl-ml-7 gl-pb-5"
/>
<source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
index 871aa880b36..bfb1517be81 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
@@ -23,7 +23,7 @@ query getState($projectPath: ID!, $iid: String!) {
userPermissions {
canMerge
}
- workInProgress
+ draft
}
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql
index daf21e75b3b..e0215fbd969 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql
@@ -1,6 +1,6 @@
#import "./auto_merge_enabled.fragment.graphql"
-query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) {
+query autoMergeEnabled($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
...autoMergeEnabled
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
index 186c0e64561..e66ac01ab12 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
@@ -1,4 +1,4 @@
-query workInProgressQuery($projectPath: ID!, $iid: String!) {
+query workInProgress($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
shouldBeRebased
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
new file mode 100644
index 00000000000..0983c28448e
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
@@ -0,0 +1,9 @@
+query mrUserPermission($projectPath: ID!, $iid: String!) {
+ project(fullPath: $projectPath) {
+ mergeRequest(iid: $iid) {
+ userPermissions {
+ updateMergeRequest
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql
index 3b34be73c15..21c3ffd8321 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql
@@ -1,4 +1,4 @@
-query readyToMergeQuery($projectPath: ID!, $iid: String!) {
+query getReadyToMergeStatus($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
userPermissions {
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
index 367b9ad1cdf..b2a1be5c5a9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
@@ -18,7 +18,9 @@ fragment ReadyToMerge on Project {
commitCount
diffHeadSha
userPermissions {
+ canMerge
removeSourceBranch
+ updateMergeRequest
}
targetBranch
mergeError
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql
index 78259e1f553..f713739f65a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql
@@ -1,6 +1,6 @@
#import "./ready_to_merge.fragment.graphql"
-query readyToMergeQuery($projectPath: ID!, $iid: String!) {
+query readyToMerge($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
...ReadyToMerge
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql
deleted file mode 100644
index 73e205ebf2b..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-query workInProgressQuery($projectPath: ID!, $iid: String!) {
- project(fullPath: $projectPath) {
- mergeRequest(iid: $iid) {
- userPermissions {
- updateMergeRequest
- }
- }
- }
-}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql
new file mode 100644
index 00000000000..200fb1b7ca5
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql
@@ -0,0 +1,10 @@
+mutation toggleDraftStatus($projectPath: ID!, $iid: String!, $draft: Boolean!) {
+ mergeRequestSetDraft(input: { projectPath: $projectPath, iid: $iid, draft: $draft }) {
+ mergeRequest {
+ mergeableDiscussionsState
+ title
+ draft
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql
deleted file mode 100644
index cfaa198d516..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-mutation toggleWIPStatus($projectPath: ID!, $iid: String!, $wip: Boolean!) {
- mergeRequestSetWip(input: { projectPath: $projectPath, iid: $iid, wip: $wip }) {
- mergeRequest {
- mergeableDiscussionsState
- title
- workInProgress
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index 65d78fc283c..2ae4f4da2f3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -1,14 +1,14 @@
import { stateKey } from './state_maps';
export default function deviseState() {
- if (this.hasMergeChecksFailed) {
+ if (!this.commitsCount) {
+ return stateKey.nothingToMerge;
+ } else if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
return stateKey.mergeChecksFailed;
} else if (this.projectArchived) {
return stateKey.archived;
} else if (this.branchMissing) {
return stateKey.missingBranch;
- } else if (!this.commitsCount) {
- return stateKey.nothingToMerge;
} else if (this.mergeStatus === 'unchecked' || this.mergeStatus === 'checking') {
return stateKey.checking;
} else if (this.hasConflicts) {
@@ -17,8 +17,8 @@ export default function deviseState() {
return stateKey.rebase;
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
return stateKey.pipelineFailed;
- } else if (this.workInProgress) {
- return stateKey.workInProgress;
+ } else if (this.draft) {
+ return stateKey.draft;
} else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) {
return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 6628225cd46..10a2907c81a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -164,7 +164,7 @@ export default class MergeRequestStore {
this.projectArchived = data.project_archived;
this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.shouldBeRebased = Boolean(data.should_be_rebased);
- this.workInProgress = data.work_in_progress;
+ this.draft = data.draft;
}
const currentUser = data.current_user;
@@ -207,7 +207,7 @@ export default class MergeRequestStore {
this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha;
this.shouldBeRebased = mergeRequest.shouldBeRebased;
- this.workInProgress = mergeRequest.workInProgress;
+ this.draft = mergeRequest.draft;
this.mergeRequestState = mergeRequest.state;
this.setState();
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index 4cb23407a74..9dfeaee905c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -4,7 +4,7 @@ export const stateToComponentMap = {
merging: 'mr-widget-merging',
conflicts: 'mr-widget-conflicts',
missingBranch: 'mr-widget-missing-branch',
- workInProgress: 'mr-widget-wip',
+ draft: 'mr-widget-wip',
readyToMerge: 'mr-widget-ready-to-merge',
nothingToMerge: 'mr-widget-nothing-to-merge',
notAllowedToMerge: 'mr-widget-not-allowed',
@@ -24,7 +24,7 @@ export const stateToComponentMap = {
export const statesToShowHelpWidget = [
'merging',
'conflicts',
- 'workInProgress',
+ 'draft',
'readyToMerge',
'checking',
'unresolvedDiscussions',
@@ -40,7 +40,7 @@ export const stateKey = {
nothingToMerge: 'nothingToMerge',
checking: 'checking',
conflicts: 'conflicts',
- workInProgress: 'workInProgress',
+ draft: 'draft',
pipelineFailed: 'pipelineFailed',
unresolvedDiscussions: 'unresolvedDiscussions',
pipelineBlocked: 'pipelineBlocked',
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
index f8f1613879f..6b774b2a734 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
@@ -1,5 +1,5 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
import NoteHeader from '~/notes/components/note_header.vue';
export default {
@@ -7,6 +7,9 @@ export default {
NoteHeader,
GlIcon,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
props: {
note: {
type: Object,
@@ -39,7 +42,7 @@ export default {
<div class="note-header">
<note-header :author="noteAuthor" :created-at="note.createdAt" :note-id="note.id">
- <span v-html="note.bodyHtml /* eslint-disable-line vue/no-v-html */"></span>
+ <span v-safe-html="note.bodyHtml"></span>
</note-header>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql
index da5f1a00e11..0c26fcc0ab2 100644
--- a/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql
@@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/alert_detail_item.fragment.graphql"
#import "~/graphql_shared/fragments/user.fragment.graphql"
-query alertDetails($fullPath: ID!, $alertId: String) {
+query alertDetailsAssignees($fullPath: ID!, $alertId: String) {
project(fullPath: $fullPath) {
alertManagementAlerts(iid: $alertId) {
nodes {
diff --git a/app/assets/javascripts/vue_shared/alert_details/index.js b/app/assets/javascripts/vue_shared/alert_details/index.js
index fda405c0fa5..9f1da9ae173 100644
--- a/app/assets/javascripts/vue_shared/alert_details/index.js
+++ b/app/assets/javascripts/vue_shared/alert_details/index.js
@@ -38,7 +38,6 @@ export default (selector) => {
return defaultDataIdFromObject(object);
},
},
- assumeImmutableResults: true,
}),
});
diff --git a/app/assets/javascripts/vue_shared/components/alerts_deprecation_warning.vue b/app/assets/javascripts/vue_shared/components/alerts_deprecation_warning.vue
deleted file mode 100644
index 16ca2df02c0..00000000000
--- a/app/assets/javascripts/vue_shared/components/alerts_deprecation_warning.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<script>
-import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__ } from '~/locale';
-
-export default {
- components: {
- GlAlert,
- GlLink,
- GlSprintf,
- },
- inject: ['hasManagedPrometheus'],
- i18n: {
- alertsDeprecationText: s__(
- 'Metrics|GitLab-managed Prometheus is deprecated and %{linkStart}scheduled for removal%{linkEnd}. Following this removal, your existing alerts will continue to function as part of the new cluster integration. However, you will no longer be able to add new alerts or edit existing alerts from the metrics dashboard.',
- ),
- },
- methods: {
- helpPagePath,
- },
-};
-</script>
-
-<template>
- <gl-alert
- v-if="hasManagedPrometheus"
- variant="warning"
- class="my-2"
- data-testid="alerts-deprecation-warning"
- >
- <gl-sprintf :message="$options.i18n.alertsDeprecationText">
- <template #link="{ content }">
- <gl-link
- :href="
- helpPagePath('operations/metrics/alerts.html', {
- anchor: 'managed-prometheus-instances',
- })
- "
- target="_blank"
- >
- <span>{{ content }}</span>
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index 40044e518c3..2c74d56f617 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -1,5 +1,5 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { HIGHLIGHT_CLASS_NAME } from './constants';
import ViewerMixin from './mixins';
@@ -9,6 +9,9 @@ export default {
components: {
GlIcon,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
mixins: [ViewerMixin, glFeatureFlagsMixin()],
inject: ['blobHash'],
data() {
@@ -65,7 +68,7 @@ export default {
<div class="blob-content">
<pre
class="code highlight"
- ><code :data-blob-hash="blobHash" v-html="content /* eslint-disable-line vue/no-v-html */"></code></pre>
+ ><code v-safe-html="content" :data-blob-hash="blobHash"></code></pre>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index f388a468fd2..5de71c35be9 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -21,6 +21,7 @@ import CiIcon from './ci_icon.vue';
* - Job show view - header
* - MR widget
* - Terraform table
+ * - On-demand scans list
*/
export default {
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
new file mode 100644
index 00000000000..4c07cf44fed
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { CONFIRM_DANGER_MODAL_ID } from './constants';
+import ConfirmDangerModal from './confirm_danger_modal.vue';
+
+export default {
+ name: 'ConfirmDanger',
+ components: {
+ GlButton,
+ ConfirmDangerModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ phrase: {
+ type: String,
+ required: true,
+ },
+ buttonText: {
+ type: String,
+ required: true,
+ },
+ buttonTestid: {
+ type: String,
+ required: false,
+ default: 'confirm-danger-button',
+ },
+ },
+ modalId: CONFIRM_DANGER_MODAL_ID,
+};
+</script>
+<template>
+ <div>
+ <gl-button
+ v-gl-modal="$options.modalId"
+ class="gl-button"
+ variant="danger"
+ :disabled="disabled"
+ :data-testid="buttonTestid"
+ >{{ buttonText }}</gl-button
+ >
+ <confirm-danger-modal
+ :modal-id="$options.modalId"
+ :phrase="phrase"
+ @confirm="$emit('confirm')"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
new file mode 100644
index 00000000000..18fa297da87
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
@@ -0,0 +1,28 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+import ConfirmDanger from './confirm_danger.vue';
+
+export default {
+ component: ConfirmDanger,
+ title: 'vue_shared/components/modals/confirm_danger_modal',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { ConfirmDanger },
+ props: Object.keys(argTypes),
+ template: '<confirm-danger v-bind="$props" />',
+ provide: {
+ confirmDangerMessage: 'You require more Vespene Gas',
+ },
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ phrase: 'You must construct additional pylons',
+ buttonText: 'Confirm button text',
+};
+
+export const Disabled = Template.bind({});
+Disabled.args = {
+ ...Default.args,
+ disabled: true,
+};
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
new file mode 100644
index 00000000000..30c96daf7e3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
@@ -0,0 +1,104 @@
+<script>
+import { GlAlert, GlModal, GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui';
+import {
+ CONFIRM_DANGER_MODAL_BUTTON,
+ CONFIRM_DANGER_MODAL_TITLE,
+ CONFIRM_DANGER_PHRASE_TEXT,
+ CONFIRM_DANGER_WARNING,
+ CONFIRM_DANGER_MODAL_ERROR,
+} from './constants';
+
+export default {
+ name: 'ConfirmDangerModal',
+ components: {
+ GlAlert,
+ GlModal,
+ GlFormGroup,
+ GlFormInput,
+ GlSprintf,
+ },
+ inject: {
+ confirmDangerMessage: {
+ default: '',
+ },
+ confirmButtonText: {
+ default: CONFIRM_DANGER_MODAL_BUTTON,
+ },
+ },
+ props: {
+ modalId: {
+ type: String,
+ required: true,
+ },
+ phrase: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return { confirmationPhrase: '' };
+ },
+ computed: {
+ isValid() {
+ return Boolean(
+ this.confirmationPhrase.length && this.equalString(this.confirmationPhrase, this.phrase),
+ );
+ },
+ actionPrimary() {
+ return {
+ text: this.confirmButtonText,
+ attributes: [{ variant: 'danger', disabled: !this.isValid }],
+ };
+ },
+ },
+ methods: {
+ equalString(a, b) {
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
+ },
+ },
+ i18n: {
+ CONFIRM_DANGER_MODAL_BUTTON,
+ CONFIRM_DANGER_MODAL_TITLE,
+ CONFIRM_DANGER_WARNING,
+ CONFIRM_DANGER_PHRASE_TEXT,
+ CONFIRM_DANGER_MODAL_ERROR,
+ },
+};
+</script>
+<template>
+ <gl-modal
+ ref="modal"
+ :modal-id="modalId"
+ :data-testid="modalId"
+ :title="$options.i18n.CONFIRM_DANGER_MODAL_TITLE"
+ :action-primary="actionPrimary"
+ @primary="$emit('confirm')"
+ >
+ <gl-alert
+ v-if="confirmDangerMessage"
+ variant="danger"
+ data-testid="confirm-danger-message"
+ :dismissible="false"
+ class="gl-mb-4"
+ >
+ {{ confirmDangerMessage }}
+ </gl-alert>
+ <p data-testid="confirm-danger-warning">{{ $options.i18n.CONFIRM_DANGER_WARNING }}</p>
+ <p data-testid="confirm-danger-phrase">
+ <gl-sprintf :message="$options.i18n.CONFIRM_DANGER_PHRASE_TEXT">
+ <template #phrase_code>
+ <code>{{ phrase }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <gl-form-group :state="isValid" :invalid-feedback="$options.i18n.CONFIRM_DANGER_MODAL_ERROR">
+ <gl-form-input
+ id="confirm_name_input"
+ v-model="confirmationPhrase"
+ class="form-control"
+ data-testid="confirm-danger-input"
+ type="text"
+ />
+ </gl-form-group>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js b/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js
new file mode 100644
index 00000000000..fa44a9be411
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js
@@ -0,0 +1,12 @@
+import { __ } from '~/locale';
+
+export const CONFIRM_DANGER_MODAL_ID = 'confirm-danger-modal';
+export const CONFIRM_DANGER_MODAL_TITLE = __('Confirmation required');
+export const CONFIRM_DANGER_MODAL_ERROR = __('Confirmation required');
+export const CONFIRM_DANGER_MODAL_BUTTON = __('Confirm');
+export const CONFIRM_DANGER_WARNING = __(
+ 'This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.',
+);
+export const CONFIRM_DANGER_PHRASE_TEXT = __(
+ 'Please type %{phrase_code} to proceed or close this modal to cancel.',
+);
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index ea507017caa..9cf8638f3cb 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -1,5 +1,8 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import {
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { forEach, escape } from 'lodash';
@@ -13,6 +16,9 @@ export default {
components: {
GlSkeletonLoading,
},
+ directives: {
+ SafeHtml,
+ },
props: {
content: {
type: String,
@@ -103,6 +109,7 @@ export default {
}
},
},
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji', 'use'] },
};
</script>
@@ -111,8 +118,8 @@ export default {
<gl-skeleton-loading v-if="isLoading" />
<div
v-else
+ v-safe-html:[$options.safeHtmlConfig]="previewContent"
class="md gl-ml-auto gl-mr-auto"
- v-html="previewContent /* eslint-disable-line vue/no-v-html */"
></div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
index 7859ef85dd8..153b0981813 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
@@ -45,7 +45,7 @@ export default {
default: false,
},
selected: {
- type: Object,
+ type: [Object, Array],
required: false,
default: () => {},
},
@@ -54,6 +54,11 @@ export default {
required: false,
default: '',
},
+ allowMultiselect: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
isSearchEmpty() {
@@ -66,8 +71,14 @@ export default {
methods: {
selectOption(option) {
this.$emit('set-option', option || null);
+ if (!this.allowMultiselect) {
+ this.$refs.dropdown.hide();
+ }
},
isSelected(option) {
+ if (Array.isArray(this.selected)) {
+ return this.selected.some((label) => label.title === option.title);
+ }
return (
this.selected &&
((option.name && this.selected.name === option.name) ||
@@ -78,7 +89,7 @@ export default {
this.$refs.dropdown.show();
},
setFocus() {
- this.$refs.search.focusInput();
+ this.$refs.search?.focusInput();
},
setSearchTerm(search) {
this.$emit('set-search', search);
@@ -108,56 +119,60 @@ export default {
@shown="setFocus"
>
<template #header>
- <gl-search-box-by-type
- ref="search"
- :value="searchTerm"
- :placeholder="searchText"
- class="js-dropdown-input-field"
- @input="setSearchTerm"
- />
+ <slot name="header">
+ <gl-search-box-by-type
+ ref="search"
+ :value="searchTerm"
+ :placeholder="searchText"
+ class="js-dropdown-input-field"
+ @input="setSearchTerm"
+ />
+ </slot>
</template>
- <gl-dropdown-form class="gl-relative gl-min-h-7">
- <gl-loading-icon
- v-if="isLoading"
- size="md"
- class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
- />
- <template v-else>
- <template v-if="isSearchEmpty && presetOptions.length > 0">
+ <slot name="default">
+ <gl-dropdown-form class="gl-relative gl-min-h-7" data-qa-selector="labels_dropdown_content">
+ <gl-loading-icon
+ v-if="isLoading"
+ size="md"
+ class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
+ />
+ <template v-else>
+ <template v-if="isSearchEmpty && presetOptions.length > 0">
+ <gl-dropdown-item
+ v-for="option in presetOptions"
+ :key="option.id"
+ :is-checked="isSelected(option)"
+ :is-check-centered="true"
+ :is-check-item="true"
+ @click.native.capture.stop="selectOption(option)"
+ >
+ <slot name="preset-item" :item="option">
+ {{ option.title }}
+ </slot>
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ </template>
<gl-dropdown-item
- v-for="option in presetOptions"
+ v-for="option in options"
:key="option.id"
:is-checked="isSelected(option)"
:is-check-centered="true"
:is-check-item="true"
- @click="selectOption(option)"
+ :avatar-url="avatarUrl(option)"
+ :secondary-text="secondaryText(option)"
+ data-testid="unselected-option"
+ @click.native.capture.stop="selectOption(option)"
>
- <slot name="preset-item" :item="option">
+ <slot name="item" :item="option">
{{ option.title }}
</slot>
</gl-dropdown-item>
- <gl-dropdown-divider />
+ <gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
+ {{ $options.i18n.noMatchingResults }}
+ </gl-dropdown-item>
</template>
- <gl-dropdown-item
- v-for="option in options"
- :key="option.id"
- :is-checked="isSelected(option)"
- :is-check-centered="true"
- :is-check-item="true"
- :avatar-url="avatarUrl(option)"
- :secondary-text="secondaryText(option)"
- data-testid="unselected-option"
- @click="selectOption(option)"
- >
- <slot name="item" :item="option">
- {{ option.title }}
- </slot>
- </gl-dropdown-item>
- <gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
- {{ $options.i18n.noMatchingResults }}
- </gl-dropdown-item>
- </template>
- </gl-dropdown-form>
+ </gl-dropdown-form>
+ </slot>
<template #footer>
<slot name="footer"></slot>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 276fb35b51f..adf34f822ed 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -86,7 +86,7 @@ export default {
<template>
<span>
<gl-loading-icon v-if="loading" size="sm" :inline="true" />
- <gl-icon v-else-if="isSymlink" name="symlink" :size="size" use-deprecated-sizes />
+ <gl-icon v-else-if="isSymlink" name="symlink" :size="size" />
<svg v-else-if="!folder" :key="spriteHref" :class="[iconSizeClass, cssClasses]">
<use v-bind="{ 'xlink:href': spriteHref }" />
</svg>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 1df65d0a666..d9290e86bca 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -53,6 +53,7 @@ export const TOKEN_TITLE_ASSIGNEE = __('Assignee');
export const TOKEN_TITLE_MILESTONE = __('Milestone');
export const TOKEN_TITLE_LABEL = __('Label');
export const TOKEN_TITLE_TYPE = __('Type');
+export const TOKEN_TITLE_RELEASE = __('Release');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
export const TOKEN_TITLE_ITERATION = __('Iteration');
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 9dc5c5db276..7c1828f2294 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -171,15 +171,17 @@ export default {
* This watcher listens for updates to `filterValue` on
* such instances. :(
*/
- filterValue(value) {
- const [firstVal] = value;
+ filterValue(newValue, oldValue) {
+ const [firstVal] = newValue;
if (
!this.initialRender &&
- value.length === 1 &&
+ newValue.length === 1 &&
firstVal.type === 'filtered-search-term' &&
!firstVal.value.data
) {
- this.$emit('onFilter', []);
+ const filtersCleared =
+ oldValue[0].type !== 'filtered-search-term' || oldValue[0].value.data !== '';
+ this.$emit('onFilter', [], filtersCleared);
}
// Set initial render flag to false
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
index ae5d3965de1..b3b3d5c88c6 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlAvatar, GlFilteredSearchSuggestion } from '@gitlab/ui';
-
+import { compact } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
@@ -59,8 +59,10 @@ export default {
.then((res) => {
// We'd want to avoid doing this check but
// users.json and /groups/:id/members & /projects/:id/users
- // return response differently.
- this.authors = Array.isArray(res) ? res : res.data;
+ // return response differently
+
+ // TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
+ this.authors = Array.isArray(res) ? compact(res) : compact(res.data);
})
.catch(() =>
createFlash({
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
index c1d1bc7da91..aff93ebc9c0 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
@@ -1,16 +1,21 @@
<script>
-import { GlFilteredSearchSuggestion } from '@gitlab/ui';
+import { GlDropdownDivider, GlDropdownSectionHeader, GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DEFAULT_ITERATIONS } from '../constants';
export default {
components: {
BaseToken,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
GlFilteredSearchSuggestion,
},
+ mixins: [glFeatureFlagMixin()],
props: {
active: {
type: Boolean,
@@ -40,6 +45,27 @@ export default {
getActiveIteration(iterations, data) {
return iterations.find((iteration) => this.getValue(iteration) === data);
},
+ groupIterationsByCadence(iterations) {
+ const cadences = [];
+ iterations.forEach((iteration) => {
+ if (!iteration.iterationCadence) {
+ return;
+ }
+ const { title } = iteration.iterationCadence;
+ const cadenceIteration = {
+ id: iteration.id,
+ title: iteration.title,
+ period: this.getIterationPeriod(iteration),
+ };
+ const cadence = cadences.find((cad) => cad.title === title);
+ if (cadence) {
+ cadence.iterations.push(cadenceIteration);
+ } else {
+ cadences.push({ title, iterations: [cadenceIteration] });
+ }
+ });
+ return cadences;
+ },
fetchIterations(searchTerm) {
this.loading = true;
this.config
@@ -57,6 +83,16 @@ export default {
getValue(iteration) {
return String(getIdFromGraphQLId(iteration.id));
},
+ /**
+ * TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/344619
+ * This method also exists as a utility function in ee/../iterations/utils.js
+ * Remove the duplication when iteration token is moved to EE.
+ */
+ getIterationPeriod({ startDate, dueDate }) {
+ const start = formatDate(startDate, 'mmm d, yyyy', true);
+ const due = formatDate(dueDate, 'mmm d, yyyy', true);
+ return `${start} - ${due}`;
+ },
},
};
</script>
@@ -77,13 +113,26 @@ export default {
{{ activeTokenValue ? activeTokenValue.title : inputValue }}
</template>
<template #suggestions-list="{ suggestions }">
- <gl-filtered-search-suggestion
- v-for="iteration in suggestions"
- :key="iteration.id"
- :value="getValue(iteration)"
- >
- {{ iteration.title }}
- </gl-filtered-search-suggestion>
+ <template v-for="(cadence, index) in groupIterationsByCadence(suggestions)">
+ <gl-dropdown-divider v-if="index !== 0" :key="index" />
+ <gl-dropdown-section-header
+ :key="cadence.title"
+ class="gl-overflow-hidden"
+ :title="cadence.title"
+ >
+ {{ cadence.title }}
+ </gl-dropdown-section-header>
+ <gl-filtered-search-suggestion
+ v-for="iteration in cadence.iterations"
+ :key="iteration.id"
+ :value="getValue(iteration)"
+ >
+ {{ iteration.title }}
+ <div v-if="glFeatures.iterationCadences" class="gl-text-gray-400">
+ {{ iteration.period }}
+ </div>
+ </gl-filtered-search-suggestion>
+ </template>
</template>
</base-token>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
new file mode 100644
index 00000000000..f353cc3a765
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
@@ -0,0 +1,85 @@
+<script>
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { DEFAULT_NONE_ANY } from '../constants';
+
+export default {
+ components: {
+ BaseToken,
+ GlFilteredSearchSuggestion,
+ },
+ props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ releases: this.config.initialReleases || [],
+ loading: false,
+ };
+ },
+ computed: {
+ defaultReleases() {
+ return this.config.defaultReleases || DEFAULT_NONE_ANY;
+ },
+ },
+ methods: {
+ getActiveRelease(releases, data) {
+ return releases.find((release) => release.tag.toLowerCase() === data.toLowerCase());
+ },
+ fetchReleases(searchTerm) {
+ this.loading = true;
+ this.config
+ .fetchReleases(searchTerm)
+ .then((response) => {
+ this.releases = response;
+ })
+ .catch(() => {
+ createFlash({ message: __('There was a problem fetching releases.') });
+ })
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <base-token
+ :active="active"
+ :config="config"
+ :value="value"
+ :default-suggestions="defaultReleases"
+ :suggestions="releases"
+ :suggestions-loading="loading"
+ :get-active-token-value="getActiveRelease"
+ @fetch-suggestions="fetchReleases"
+ v-on="$listeners"
+ >
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ {{ activeTokenValue ? activeTokenValue.tag : inputValue }}
+ </template>
+ <template #suggestions-list="{ suggestions }">
+ <gl-filtered-search-suggestion
+ v-for="release in suggestions"
+ :key="release.id"
+ :value="release.tag"
+ >
+ {{ release.tag }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </base-token>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 41613bb3307..6ace0bd88f8 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,10 +1,16 @@
<script>
-import { GlTooltipDirective, GlLink, GlButton, GlTooltip, GlSafeHtmlDirective } from '@gitlab/ui';
+import {
+ GlTooltipDirective,
+ GlButton,
+ GlSafeHtmlDirective,
+ GlAvatarLink,
+ GlAvatarLabeled,
+} from '@gitlab/ui';
+import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '../../emoji';
import { __, sprintf } from '../../locale';
import CiIconBadge from './ci_badge_link.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
-import UserAvatarImage from './user_avatar/user_avatar_image.vue';
/**
* Renders header component for job and pipeline page based on UI mockups
@@ -17,10 +23,9 @@ export default {
components: {
CiIconBadge,
TimeagoTooltip,
- UserAvatarImage,
- GlLink,
GlButton,
- GlTooltip,
+ GlAvatarLink,
+ GlAvatarLabeled,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -94,6 +99,9 @@ export default {
return this.itemName;
},
+ userId() {
+ return isGid(this.user?.id) ? getIdFromGraphQLId(this.user?.id) : this.user?.id;
+ },
},
methods: {
@@ -124,24 +132,32 @@ export default {
{{ __('by') }}
<template v-if="user">
- <gl-link
- v-gl-tooltip
- :href="userPath"
- :title="user.email"
- class="js-user-link commit-committer-link"
+ <gl-avatar-link
+ :data-user-id="userId"
+ :data-username="user.username"
+ :data-name="user.name"
+ :href="user.webUrl"
+ target="_blank"
+ class="js-user-link gl-vertical-align-middle gl-mx-2 gl-align-items-center"
>
- <user-avatar-image :img-src="avatarUrl" :img-alt="userAvatarAltText" :size="24" />
- {{ user.name }}
- </gl-link>
- <gl-tooltip v-if="message" :target="() => $refs[$options.EMOJI_REF]">
- {{ message }}
- </gl-tooltip>
- <span
- v-if="statusTooltipHTML"
- :ref="$options.EMOJI_REF"
- v-safe-html:[$options.safeHtmlConfig]="statusTooltipHTML"
- :data-testid="message"
- ></span>
+ <gl-avatar-labeled
+ :size="24"
+ :src="avatarUrl"
+ :label="user.name"
+ class="gl-display-none gl-sm-display-inline-flex gl-mx-1"
+ />
+ <strong class="author gl-display-inline gl-sm-display-none!">@{{ user.username }}</strong>
+ <gl-tooltip v-if="message" :target="() => $refs[$options.EMOJI_REF]">
+ {{ message }}
+ </gl-tooltip>
+ <span
+ v-if="statusTooltipHTML"
+ :ref="$options.EMOJI_REF"
+ v-safe-html:[$options.safeHtmlConfig]="statusTooltipHTML"
+ class="gl-ml-2"
+ :data-testid="message"
+ ></span>
+ </gl-avatar-link>
</template>
</section>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index f9ae59567b2..648e9c9462f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -1,11 +1,11 @@
<script>
-import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlBadge, GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import ApplySuggestion from './apply_suggestion.vue';
export default {
- components: { GlIcon, GlButton, GlLoadingIcon, ApplySuggestion },
+ components: { GlBadge, GlIcon, GlButton, GlLoadingIcon, ApplySuggestion },
directives: { 'gl-tooltip': GlTooltipDirective },
props: {
batchSuggestionsCount: {
@@ -134,8 +134,14 @@ export default {
<gl-icon name="question-o" css-classes="link-highlight" />
</a>
</div>
- <div v-if="isApplied" class="badge badge-success">{{ __('Applied') }}</div>
- <div v-else-if="isApplying" class="d-flex align-items-center text-secondary">
+ <gl-badge v-if="isApplied" variant="success" data-qa-selector="applied_badge">
+ {{ __('Applied') }}
+ </gl-badge>
+ <div
+ v-else-if="isApplying"
+ class="d-flex align-items-center text-secondary"
+ data-qa-selector="applying_badge"
+ >
<gl-loading-icon size="sm" class="d-flex-center mr-2" />
<span>{{ applyingSuggestionsMessage }}</span>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index 755e6f1f224..8877cfa39fb 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -26,6 +26,7 @@ import {
import $ from 'jquery';
import { mapGetters, mapActions, mapState } from 'vuex';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
+import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import initMRPopovers from '~/mr_popover/';
import noteHeader from '~/notes/components/note_header.vue';
@@ -61,6 +62,9 @@ export default {
data() {
return {
expanded: false,
+ lines: [],
+ showLines: false,
+ loadingDiff: false,
};
},
computed: {
@@ -94,10 +98,25 @@ export default {
},
methods: {
...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']),
+ async toggleDiff() {
+ this.showLines = !this.showLines;
+
+ if (!this.lines.length) {
+ this.loadingDiff = true;
+ const { data } = await axios.get(this.note.outdated_line_change_path);
+
+ this.lines = data.map((l) => ({
+ ...l,
+ rich_text: l.rich_text.replace(/^[+ -]/, ''),
+ }));
+ this.loadingDiff = false;
+ }
+ },
},
safeHtmlConfig: {
ADD_TAGS: ['use'], // to support icon SVGs
},
+ userColorSchemeClass: window.gon.user_color_scheme,
};
</script>
@@ -112,15 +131,28 @@ export default {
<div class="note-header">
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
<span v-safe-html="actionTextHtml"></span>
- <template v-if="canSeeDescriptionVersion" #extra-controls>
+ <template
+ v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
+ #extra-controls
+ >
&middot;
<gl-button
+ v-if="canSeeDescriptionVersion"
variant="link"
:icon="descriptionVersionToggleIcon"
data-testid="compare-btn"
@click="toggleDescriptionVersion"
>{{ __('Compare with previous version') }}</gl-button
>
+ <gl-button
+ v-if="note.outdated_line_change_path"
+ :icon="showLines ? 'chevron-up' : 'chevron-down'"
+ variant="link"
+ data-testid="outdated-lines-change-btn"
+ @click="toggleDiff"
+ >
+ {{ __('Compare changes') }}
+ </gl-button>
</template>
</note-header>
</div>
@@ -154,6 +186,37 @@ export default {
@click="deleteDescriptionVersion"
/>
</div>
+ <div
+ v-if="lines.length && showLines"
+ class="diff-content gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden"
+ >
+ <table
+ :class="$options.userColorSchemeClass"
+ class="code js-syntax-highlight"
+ data-testid="outdated-lines"
+ >
+ <tr v-for="line in lines" v-once :key="line.line_code" class="line_holder">
+ <td
+ :class="line.type"
+ class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0!"
+ >
+ {{ line.old_line }}
+ </td>
+ <td
+ :class="line.type"
+ class="diff-line-num new_line gl-border-bottom-0! gl-border-top-0!"
+ >
+ {{ line.new_line }}
+ </td>
+ <td
+ :class="line.type"
+ class="line_content gl-display-table-cell!"
+ v-html="line.rich_text /* eslint-disable-line vue/no-v-html */"
+ ></td>
+ </tr>
+ </table>
+ </div>
+ <gl-skeleton-loading v-else-if="showLines" class="gl-mt-4" />
</div>
</div>
</timeline-entry-item>
diff --git a/app/assets/javascripts/vue_shared/components/registry/title_area.vue b/app/assets/javascripts/vue_shared/components/registry/title_area.vue
index 4b21ec0330a..d108d8d689d 100644
--- a/app/assets/javascripts/vue_shared/components/registry/title_area.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/title_area.vue
@@ -69,20 +69,23 @@ export default {
/>
<div class="gl-display-flex gl-flex-direction-column">
- <h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2" data-testid="title">
+ <h2 class="gl-font-size-h1 gl-mt-3 gl-mb-0" data-testid="title">
<slot name="title">{{ title }}</slot>
- </h1>
+ </h2>
<div
v-if="$slots['sub-header']"
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
<slot name="sub-header"></slot>
</div>
</div>
</div>
- <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3">
+ <div
+ v-if="metadataSlots.length > 0"
+ class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
+ >
<template v-if="!metadataLoading">
<div
v-for="(row, metadataIndex) in metadataSlots"
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
index facace0d809..34845e3d9e4 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
@@ -2,6 +2,8 @@ import { s__ } from '~/locale';
export const PLATFORMS_WITHOUT_ARCHITECTURES = ['docker', 'kubernetes'];
+export const REGISTRATION_TOKEN_PLACEHOLDER = '$REGISTRATION_TOKEN';
+
export const INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES = {
docker: {
instructions: s__(
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
index d55c93fd146..d5493aa5a66 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
@@ -16,8 +16,9 @@ import { isEmpty } from 'lodash';
import { __, s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import {
- PLATFORMS_WITHOUT_ARCHITECTURES,
INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
+ PLATFORMS_WITHOUT_ARCHITECTURES,
+ REGISTRATION_TOKEN_PLACEHOLDER,
} from './constants';
import getRunnerPlatformsQuery from './graphql/queries/get_runner_platforms.query.graphql';
import getRunnerSetupInstructionsQuery from './graphql/queries/get_runner_setup.query.graphql';
@@ -41,7 +42,13 @@ export default {
props: {
modalId: {
type: String,
- required: true,
+ required: false,
+ default: 'runner-instructions-modal',
+ },
+ registrationToken: {
+ type: String,
+ required: false,
+ default: null,
},
},
apollo: {
@@ -117,8 +124,20 @@ export default {
runnerInstallationLink() {
return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatformName]?.link;
},
+ registerInstructionsWithToken() {
+ const { registerInstructions } = this.instructions || {};
+
+ if (this.registrationToken) {
+ return registerInstructions.replace(REGISTRATION_TOKEN_PLACEHOLDER, this.registrationToken);
+ }
+
+ return registerInstructions;
+ },
},
methods: {
+ show() {
+ this.$refs.modal.show();
+ },
selectPlatform(platform) {
this.selectedPlatform = platform;
@@ -158,9 +177,11 @@ export default {
</script>
<template>
<gl-modal
+ ref="modal"
:modal-id="modalId"
:title="$options.i18n.installARunner"
:action-secondary="$options.closeButton"
+ v-bind="$attrs"
>
<gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
{{ $options.i18n.fetchError }}
@@ -243,11 +264,11 @@ export default {
<pre
class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line"
data-testid="register-command"
- >{{ instructions.registerInstructions }}</pre
+ >{{ registerInstructionsWithToken }}</pre
>
<modal-copy-button
:title="$options.i18n.copyInstructions"
- :text="instructions.registerInstructions"
+ :text="registerInstructionsWithToken"
:modal-id="$options.modalId"
css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
category="tertiary"
diff --git a/app/assets/javascripts/vue_shared/components/settings/settings_block.vue b/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
index e75fedbb1d7..e68f0f31c13 100644
--- a/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
+++ b/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
@@ -24,10 +24,13 @@ export default {
},
},
data() {
+ const forceOpen = !this.collapsible || this.defaultExpanded;
return {
// Non-collapsible sections should always be expanded.
// For collapsible sections, fall back to defaultExpanded.
- sectionExpanded: !this.collapsible || this.defaultExpanded,
+ sectionExpanded: forceOpen,
+ initialised: forceOpen,
+ animating: false,
};
},
computed: {
@@ -53,7 +56,12 @@ export default {
toggleSectionExpanded() {
this.sectionExpanded = !this.sectionExpanded;
+ if (!this.initialised) {
+ this.initialised = true;
+ }
+
if (this.sectionExpanded) {
+ this.animating = true;
this.$refs.settingsContent.focus();
}
},
@@ -68,7 +76,10 @@ export default {
</script>
<template>
- <section class="settings" :class="{ 'no-animate': !slideAnimated, expanded: sectionExpanded }">
+ <section
+ class="settings"
+ :class="{ 'no-animate': !slideAnimated, expanded: sectionExpanded, animating }"
+ >
<div class="settings-header">
<h4>
<span
@@ -103,12 +114,14 @@ export default {
</p>
</div>
<div
+ v-show="initialised"
:id="settingsContentId"
ref="settingsContent"
:aria-labelledby="settingsLabelId"
tabindex="-1"
role="region"
class="settings-content"
+ @animationend="animating = false"
>
<slot></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
index 6511c8d8c31..460a10e08ed 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
@@ -40,7 +40,7 @@ export default {
</script>
<template>
- <div v-gl-tooltip.left.viewport :class="containerClass" :title="tooltipText" @click="click">
+ <div v-gl-tooltip.left.viewport="tooltipText" :class="containerClass" @click="click">
<gl-icon v-if="showIcon" name="calendar" />
<slot>
<span> {{ text }} </span>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
index d80b66fd9be..399db978b60 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
@@ -36,6 +36,7 @@ export default {
<template>
<div
class="labels-select-dropdown-contents gl-w-full gl-my-2 gl-py-3 gl-rounded-base gl-absolute"
+ data-testid="labels-select-dropdown-contents"
data-qa-selector="labels_dropdown_content"
:style="directionStyle"
>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
index 122250d1ce7..8a26c4a6618 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
@@ -43,12 +43,7 @@ export default {
</script>
<template>
- <div
- v-gl-tooltip.left.viewport
- :title="labelsList"
- class="sidebar-collapsed-icon"
- @click="handleClick"
- >
+ <div v-gl-tooltip.left.viewport="labelsList" class="sidebar-collapsed-icon" @click="handleClick">
<gl-icon name="labels" />
<span>{{ labels.length }}</span>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 0ea22eb7aea..9e64f03fe84 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -1,5 +1,4 @@
import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
-import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import { DropdownVariant } from '../constants';
import * as types from './mutation_types';
@@ -67,9 +66,11 @@ export default {
}
if (isScopedLabel(candidateLabel)) {
- const scopedKeyWithDelimiter = `${scopedLabelKey(candidateLabel)}${SCOPED_LABEL_DELIMITER}`;
const currentActiveScopedLabel = state.labels.find(
- ({ title }) => title.startsWith(scopedKeyWithDelimiter) && title !== candidateLabel.title,
+ ({ set, title }) =>
+ set &&
+ title !== candidateLabel.title &&
+ scopedLabelKey({ title }) === scopedLabelKey(candidateLabel),
);
if (currentActiveScopedLabel) {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
index 389eb174c0e..cd671b4d8f5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
@@ -1,7 +1,13 @@
export const SCOPED_LABEL_DELIMITER = '::';
+export const DEBOUNCE_DROPDOWN_DELAY = 200;
export const DropdownVariant = {
Sidebar: 'sidebar',
Standalone: 'standalone',
Embedded: 'embedded',
};
+
+export const LabelType = {
+ group: 'group',
+ project: 'project',
+};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index 3ee0baf8812..f7485de0342 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -1,20 +1,25 @@
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
+import DropdownFooter from './dropdown_footer.vue';
+import DropdownHeader from './dropdown_header.vue';
import { isDropdownVariantStandalone, isDropdownVariantSidebar } from './utils';
export default {
components: {
DropdownContentsLabelsView,
DropdownContentsCreateView,
+ DropdownHeader,
+ DropdownFooter,
GlButton,
GlDropdown,
GlDropdownItem,
GlLink,
},
- inject: ['allowLabelCreate', 'labelsManagePath'],
props: {
labelsCreateTitle: {
type: String,
@@ -48,10 +53,6 @@ export default {
type: String,
required: true,
},
- issuableType: {
- type: String,
- required: true,
- },
isVisible: {
type: Boolean,
required: false,
@@ -61,10 +62,17 @@ export default {
type: String,
required: true,
},
+ workspaceType: {
+ type: String,
+ required: true,
+ },
attrWorkspacePath: {
type: String,
- required: false,
- default: undefined,
+ required: true,
+ },
+ labelCreateType: {
+ type: String,
+ required: true,
},
},
data() {
@@ -72,6 +80,7 @@ export default {
showDropdownContentsCreateView: false,
localSelectedLabels: [...this.selectedLabels],
isDirty: false,
+ searchKey: '',
};
},
computed: {
@@ -113,15 +122,24 @@ export default {
if (newVal) {
this.$refs.dropdown.show();
this.isDirty = false;
+ this.localSelectedLabels = this.selectedLabels;
} else {
this.$refs.dropdown.hide();
this.setLabels();
}
},
selectedLabels(newVal) {
- this.localSelectedLabels = newVal;
+ if (!this.isDirty) {
+ this.localSelectedLabels = newVal;
+ }
},
},
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ beforeDestroy() {
+ this.debouncedSearchKeyUpdate.cancel();
+ },
methods: {
toggleDropdownContentsCreateView() {
this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView;
@@ -140,10 +158,20 @@ export default {
this.$emit('setLabels', this.localSelectedLabels);
},
handleDropdownHide() {
+ this.$emit('closeDropdown');
if (!isDropdownVariantSidebar(this.variant)) {
this.setLabels();
}
},
+ setSearchKey(value) {
+ this.searchKey = value;
+ },
+ setFocus() {
+ this.$refs.header.focusInput();
+ },
+ showDropdown() {
+ this.$refs.dropdown.show();
+ },
},
};
</script>
@@ -153,62 +181,44 @@ export default {
ref="dropdown"
:text="buttonText"
class="gl-w-full gl-mt-2"
+ data-testid="labels-select-dropdown-contents"
data-qa-selector="labels_dropdown_content"
@hide="handleDropdownHide"
+ @shown="setFocus"
>
<template #header>
- <div
+ <dropdown-header
v-if="!isStandalone"
- class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
- data-testid="dropdown-header"
- >
- <gl-button
- v-if="showDropdownContentsCreateView"
- :aria-label="__('Go back')"
- variant="link"
- size="small"
- class="js-btn-back dropdown-header-button gl-p-0"
- icon="arrow-left"
- data-testid="go-back-button"
- @click.stop="toggleDropdownContent"
- />
- <span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
- <gl-button
- :aria-label="__('Close')"
- variant="link"
- size="small"
- class="dropdown-header-button gl-p-0!"
- icon="close"
- data-testid="close-button"
- @click="$emit('closeDropdown')"
- />
- </div>
+ ref="header"
+ v-model="searchKey"
+ :labels-create-title="labelsCreateTitle"
+ :labels-list-title="labelsListTitle"
+ :show-dropdown-contents-create-view="showDropdownContentsCreateView"
+ @toggleDropdownContentsCreateView="toggleDropdownContent"
+ @closeDropdown="$emit('closeDropdown')"
+ @input="debouncedSearchKeyUpdate"
+ />
</template>
<template #default>
<component
:is="dropdownContentsView"
v-model="localSelectedLabels"
- :selected-labels="selectedLabels"
+ :search-key="searchKey"
:allow-multiselect="allowMultiselect"
- :issuable-type="issuableType"
:full-path="fullPath"
+ :workspace-type="workspaceType"
:attr-workspace-path="attrWorkspacePath"
- @hideCreateView="toggleDropdownContentsCreateView"
+ :label-create-type="labelCreateType"
+ @hideCreateView="toggleDropdownContent"
/>
</template>
<template #footer>
- <div v-if="showDropdownFooter" data-testid="dropdown-footer">
- <gl-dropdown-item
- v-if="allowLabelCreate"
- data-testid="create-label-button"
- @click.capture.native.stop="toggleDropdownContent"
- >
- {{ footerCreateLabelTitle }}
- </gl-dropdown-item>
- <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
- {{ footerManageLabelTitle }}
- </gl-dropdown-item>
- </div>
+ <dropdown-footer
+ v-if="showDropdownFooter"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
+ @toggleDropdownContentsCreateView="toggleDropdownContent"
+ />
</template>
</gl-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index a2ed08e6b28..da626a21b14 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -2,10 +2,10 @@
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import produce from 'immer';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
-import { labelsQueries } from '~/sidebar/constants';
+import { workspaceLabelsQueries } from '~/sidebar/constants';
import createLabelMutation from './graphql/create_label.mutation.graphql';
+import { LabelType } from './constants';
const errorMessage = __('Error creating label.');
@@ -20,18 +20,21 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
- issuableType: {
+ fullPath: {
type: String,
required: true,
},
- fullPath: {
+ attrWorkspacePath: {
type: String,
required: true,
},
- attrWorkspacePath: {
+ labelCreateType: {
+ type: String,
+ required: true,
+ },
+ workspaceType: {
type: String,
- required: false,
- default: undefined,
+ required: true,
},
},
data() {
@@ -50,25 +53,13 @@ export default {
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
},
mutationVariables() {
- if (this.issuableType === IssuableType.Epic) {
- return {
- title: this.labelTitle,
- color: this.selectedColor,
- groupPath: this.fullPath,
- };
- }
+ const attributePath = this.labelCreateType === LabelType.group ? 'groupPath' : 'projectPath';
- return this.attrWorkspacePath !== undefined
- ? {
- title: this.labelTitle,
- color: this.selectedColor,
- groupPath: this.attrWorkspacePath,
- }
- : {
- title: this.labelTitle,
- color: this.selectedColor,
- projectPath: this.fullPath,
- };
+ return {
+ title: this.labelTitle,
+ color: this.selectedColor,
+ [attributePath]: this.attrWorkspacePath,
+ };
},
},
methods: {
@@ -82,8 +73,10 @@ export default {
this.selectedColor = this.getColorCode(color);
},
updateLabelsInCache(store, label) {
+ const { query } = workspaceLabelsQueries[this.workspaceType];
+
const sourceData = store.readQuery({
- query: labelsQueries[this.issuableType].workspaceQuery,
+ query,
variables: { fullPath: this.fullPath, searchTerm: '' },
});
@@ -95,7 +88,7 @@ export default {
});
store.writeQuery({
- query: labelsQueries[this.issuableType].workspaceQuery,
+ query,
variables: { fullPath: this.fullPath, searchTerm: '' },
data,
});
@@ -180,7 +173,7 @@ export default {
<gl-button
class="js-btn-cancel-create"
data-testid="cancel-button"
- @click="$emit('hideCreateView')"
+ @click.stop="$emit('hideCreateView')"
>
{{ __('Cancel') }}
</gl-button>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index e6a25362ff0..e9a2d7747e2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -1,18 +1,10 @@
<script>
-import {
- GlDropdownForm,
- GlDropdownItem,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlIntersectionObserver,
-} from '@gitlab/ui';
+import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { debounce } from 'lodash';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale';
-import { labelsQueries } from '~/sidebar/constants';
+import { workspaceLabelsQueries } from '~/sidebar/constants';
import LabelItem from './label_item.vue';
export default {
@@ -20,7 +12,6 @@ export default {
GlDropdownForm,
GlDropdownItem,
GlLoadingIcon,
- GlSearchBoxByType,
GlIntersectionObserver,
LabelItem,
},
@@ -28,18 +19,10 @@ export default {
prop: 'localSelectedLabels',
},
props: {
- selectedLabels: {
- type: Array,
- required: true,
- },
allowMultiselect: {
type: Boolean,
required: true,
},
- issuableType: {
- type: String,
- required: true,
- },
localSelectedLabels: {
type: Array,
required: true,
@@ -48,10 +31,17 @@ export default {
type: String,
required: true,
},
+ searchKey: {
+ type: String,
+ required: true,
+ },
+ workspaceType: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
- searchKey: '',
labels: [],
isVisible: false,
};
@@ -59,7 +49,7 @@ export default {
apollo: {
labels: {
query() {
- return labelsQueries[this.issuableType].workspaceQuery;
+ return workspaceLabelsQueries[this.workspaceType].query;
},
variables() {
return {
@@ -71,12 +61,6 @@ export default {
return this.searchKey.length === 1 || !this.isVisible;
},
update: (data) => data.workspace?.labels?.nodes || [],
- async result() {
- if (this.$refs.searchInput) {
- await this.$nextTick;
- this.$refs.searchInput.focusInput();
- }
- },
error() {
createFlash({ message: __('Error fetching labels.') });
},
@@ -101,12 +85,6 @@ export default {
return Boolean(this.searchKey) && this.visibleLabels.length === 0;
},
},
- created() {
- this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- },
- beforeDestroy() {
- this.debouncedSearchKeyUpdate.cancel();
- },
methods: {
isLabelSelected(label) {
return this.localSelectedLabelsIds.includes(getIdFromGraphQLId(label.id));
@@ -137,13 +115,7 @@ export default {
({ id }) => id !== getIdFromGraphQLId(label.id) && id !== label.id,
);
} else {
- labels = [
- ...this.localSelectedLabels,
- {
- ...label,
- id: getIdFromGraphQLId(label.id),
- },
- ];
+ labels = [...this.localSelectedLabels, label];
}
this.$emit('input', labels);
},
@@ -153,12 +125,8 @@ export default {
this.$emit('closeDropdown', this.localSelectedLabels);
}
},
- setSearchKey(value) {
- this.searchKey = value;
- },
onDropdownAppear() {
this.isVisible = true;
- this.$refs.searchInput.focusInput();
},
},
};
@@ -167,14 +135,6 @@ export default {
<template>
<gl-intersection-observer @appear="onDropdownAppear">
<gl-dropdown-form class="labels-select-contents-list js-labels-list">
- <gl-search-box-by-type
- ref="searchInput"
- :value="searchKey"
- :disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
- data-testid="dropdown-input-field"
- @input="debouncedSearchKeyUpdate"
- />
<div ref="labelsListContainer" data-testid="dropdown-content">
<gl-loading-icon
v-if="labelsFetchInProgress"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue
new file mode 100644
index 00000000000..e67e704ffb8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ inject: ['allowLabelCreate', 'labelsManagePath'],
+ props: {
+ footerCreateLabelTitle: {
+ type: String,
+ required: true,
+ },
+ footerManageLabelTitle: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div data-testid="dropdown-footer">
+ <gl-dropdown-item
+ v-if="allowLabelCreate"
+ data-testid="create-label-button"
+ @click.capture.native.stop="$emit('toggleDropdownContentsCreateView')"
+ >
+ {{ footerCreateLabelTitle }}
+ </gl-dropdown-item>
+ <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
+ {{ footerManageLabelTitle }}
+ </gl-dropdown-item>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
new file mode 100644
index 00000000000..10064b01648
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlButton, GlSearchBoxByType } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ GlSearchBoxByType,
+ },
+ model: {
+ prop: 'searchKey',
+ },
+ props: {
+ labelsCreateTitle: {
+ type: String,
+ required: true,
+ },
+ labelsListTitle: {
+ type: String,
+ required: true,
+ },
+ showDropdownContentsCreateView: {
+ type: Boolean,
+ required: true,
+ },
+ labelsFetchInProgress: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ searchKey: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ dropdownTitle() {
+ return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle;
+ },
+ },
+ methods: {
+ focusInput() {
+ this.$refs.searchInput?.focusInput();
+ },
+ },
+};
+</script>
+
+<template>
+ <div data-testid="dropdown-header">
+ <div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!">
+ <gl-button
+ v-if="showDropdownContentsCreateView"
+ :aria-label="__('Go back')"
+ variant="link"
+ size="small"
+ class="js-btn-back dropdown-header-button gl-p-0"
+ icon="arrow-left"
+ data-testid="go-back-button"
+ @click.stop="$emit('toggleDropdownContentsCreateView')"
+ />
+ <span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
+ <gl-button
+ :aria-label="__('Close')"
+ variant="link"
+ size="small"
+ class="dropdown-header-button gl-p-0!"
+ icon="close"
+ data-testid="close-button"
+ @click="$emit('closeDropdown')"
+ />
+ </div>
+ <gl-search-box-by-type
+ v-if="!showDropdownContentsCreateView"
+ ref="searchInput"
+ :value="searchKey"
+ :disabled="labelsFetchInProgress"
+ data-qa-selector="dropdown_input_field"
+ data-testid="dropdown-input-field"
+ @input="$emit('input', $event)"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
index 71d3d87cce5..aed5bc303ee 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
@@ -1,7 +1,6 @@
<script>
import { GlLabel } from '@gitlab/ui';
import { sortBy } from 'lodash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
@@ -47,7 +46,7 @@ export default {
return this.allowScopedLabels && isScopedLabel(label);
},
removeLabel(labelId) {
- this.$emit('onLabelRemove', getIdFromGraphQLId(labelId));
+ this.$emit('onLabelRemove', labelId);
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
index eb478645a03..a9c791091fc 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
@@ -1,12 +1,11 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
mutation createLabel($title: String!, $color: String, $projectPath: ID, $groupPath: ID) {
labelCreate(
input: { title: $title, color: $color, projectPath: $projectPath, groupPath: $groupPath }
) {
label {
- id
- color
- description
- title
+ ...Label
}
errors
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
index a2e8579486f..c130cc426dc 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
@@ -1,13 +1,12 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
query epicLabels($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
issuable: epic(iid: $iid) {
id
labels {
nodes {
- id
- title
- color
- description
+ ...Label
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql
new file mode 100644
index 00000000000..45fcb50732e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql
@@ -0,0 +1,15 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
+mutation updateEpicLabels($input: UpdateEpicInput!) {
+ updateEpic(input: $input) {
+ epic {
+ id
+ labels {
+ nodes {
+ ...Label
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
index acc9bcd2015..ce1a69f84c0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
@@ -1,11 +1,11 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
query groupLabels($fullPath: ID!, $searchTerm: String) {
workspace: group(fullPath: $fullPath) {
- labels(searchTerm: $searchTerm, onlyGroupLabels: true) {
+ id
+ labels(searchTerm: $searchTerm, onlyGroupLabels: true, includeAncestorGroups: true) {
nodes {
- id
- title
- color
- description
+ ...Label
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
index 1c2fd3bb7c0..e471d279b24 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
@@ -1,13 +1,12 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
query issueLabels($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
issuable: issue(iid: $iid) {
id
labels {
nodes {
- id
- title
- color
- description
+ ...Label
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql
new file mode 100644
index 00000000000..dd80e89c8a7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql
@@ -0,0 +1,14 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
+query mergeRequestLabels($fullPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $fullPath) {
+ issuable: mergeRequest(iid: $iid) {
+ id
+ labels {
+ nodes {
+ ...Label
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
index dc39220487d..a7c24620aad 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
@@ -1,11 +1,11 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
query projectLabels($fullPath: ID!, $searchTerm: String) {
workspace: project(fullPath: $fullPath) {
+ id
labels(searchTerm: $searchTerm, includeAncestorGroups: true) {
nodes {
- id
- title
- color
- description
+ ...Label
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 6bd43da2203..97a65c13933 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -1,9 +1,12 @@
<script>
+import { debounce } from 'lodash';
+import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils';
import createFlash from '~/flash';
+import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { labelsQueries } from '~/sidebar/constants';
-import { DropdownVariant } from './constants';
+import { issuableLabelsQueries } from '~/sidebar/constants';
+import { DEBOUNCE_DROPDOWN_DELAY, DropdownVariant } from './constants';
import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
@@ -50,16 +53,6 @@ export default {
required: false,
default: DropdownVariant.Sidebar,
},
- selectedLabels: {
- type: Array,
- required: false,
- default: () => [],
- },
- labelsSelectInProgress: {
- type: Boolean,
- required: false,
- default: false,
- },
labelsFilterBasePath: {
type: String,
required: false,
@@ -95,36 +88,44 @@ export default {
required: false,
default: __('Manage group labels'),
},
- isEditing: {
- type: Boolean,
- required: false,
- default: false,
- },
issuableType: {
type: String,
required: true,
},
+ workspaceType: {
+ type: String,
+ required: true,
+ },
attrWorkspacePath: {
type: String,
- required: false,
- default: undefined,
+ required: true,
+ },
+ labelCreateType: {
+ type: String,
+ required: true,
},
},
data() {
return {
contentIsOnViewport: true,
issuableLabels: [],
+ labelsSelectInProgress: false,
+ oldIid: null,
+ sidebarExpandedOnClick: false,
};
},
computed: {
isLoading() {
return this.labelsSelectInProgress || this.$apollo.queries.issuableLabels.loading;
},
+ issuableLabelIds() {
+ return this.issuableLabels.map((label) => label.id);
+ },
},
apollo: {
issuableLabels: {
query() {
- return labelsQueries[this.issuableType].issuableQuery;
+ return issuableLabelsQueries[this.issuableType].issuableQuery;
},
skip() {
return !isDropdownVariantSidebar(this.variant);
@@ -143,16 +144,140 @@ export default {
},
},
},
+ watch: {
+ iid(_, oldVal) {
+ this.oldIid = oldVal;
+ },
+ },
+ mounted() {
+ document.addEventListener('toggleSidebarRevealLabelsDropdown', this.handleCollapsedValueClick);
+ },
+ beforeDestroy() {
+ document.removeEventListener(
+ 'toggleSidebarRevealLabelsDropdown',
+ this.handleCollapsedValueClick,
+ );
+ },
methods: {
handleDropdownClose(labels) {
- this.$emit('updateSelectedLabels', labels);
+ if (this.iid !== '') {
+ this.updateSelectedLabels(this.getUpdateVariables(labels));
+ } else {
+ this.$emit('updateSelectedLabels', { labels });
+ }
+
this.collapseEditableItem();
},
collapseEditableItem() {
this.$refs.editable?.collapse();
+ if (this.sidebarExpandedOnClick) {
+ this.sidebarExpandedOnClick = false;
+ this.$emit('toggleCollapse');
+ }
},
handleCollapsedValueClick() {
+ this.sidebarExpandedOnClick = true;
this.$emit('toggleCollapse');
+ debounce(() => {
+ this.$refs.editable.toggle();
+ this.$refs.dropdownContents.showDropdown();
+ }, DEBOUNCE_DROPDOWN_DELAY)();
+ },
+ getUpdateVariables(labels) {
+ let labelIds = [];
+
+ labelIds = labels.map(({ id }) => id);
+ const currentIid = this.oldIid || this.iid;
+
+ const updateVariables = {
+ iid: currentIid,
+ projectPath: this.fullPath,
+ labelIds,
+ };
+
+ switch (this.issuableType) {
+ case IssuableType.Issue:
+ return updateVariables;
+ case IssuableType.MergeRequest:
+ return {
+ ...updateVariables,
+ operationMode: MutationOperationMode.Replace,
+ };
+ case IssuableType.Epic:
+ return {
+ iid: currentIid,
+ groupPath: this.fullPath,
+ addLabelIds: labelIds.map((id) => getIdFromGraphQLId(id)),
+ removeLabelIds: this.issuableLabelIds
+ .filter((id) => !labelIds.includes(id))
+ .map((id) => getIdFromGraphQLId(id)),
+ };
+ default:
+ return {};
+ }
+ },
+ updateSelectedLabels(inputVariables) {
+ this.labelsSelectInProgress = true;
+
+ this.$apollo
+ .mutate({
+ mutation: issuableLabelsQueries[this.issuableType].mutation,
+ variables: { input: inputVariables },
+ })
+ .then(({ data }) => {
+ const { mutationName } = issuableLabelsQueries[this.issuableType];
+
+ if (data[mutationName]?.errors?.length) {
+ throw new Error();
+ }
+
+ this.$emit('updateSelectedLabels', {
+ id: data[mutationName]?.[this.issuableType]?.id,
+ labels: data[mutationName]?.[this.issuableType]?.labels?.nodes,
+ });
+ })
+ .catch((error) =>
+ createFlash({
+ message: __('An error occurred while updating labels.'),
+ captureError: true,
+ error,
+ }),
+ )
+ .finally(() => {
+ this.labelsSelectInProgress = false;
+ });
+ },
+ getRemoveVariables(labelId) {
+ const removeVariables = {
+ iid: this.iid,
+ projectPath: this.fullPath,
+ };
+
+ switch (this.issuableType) {
+ case IssuableType.Issue:
+ return {
+ ...removeVariables,
+ removeLabelIds: [labelId],
+ };
+ case IssuableType.MergeRequest:
+ return {
+ ...removeVariables,
+ labelIds: [labelId],
+ operationMode: MutationOperationMode.Remove,
+ };
+ case IssuableType.Epic:
+ return {
+ iid: this.iid,
+ removeLabelIds: [getIdFromGraphQLId(labelId)],
+ groupPath: this.fullPath,
+ };
+ default:
+ return {};
+ }
+ },
+ handleLabelRemove(labelId) {
+ this.updateSelectedLabels(this.getRemoveVariables(labelId));
+ this.$emit('onLabelRemove', labelId);
},
isDropdownVariantSidebar,
isDropdownVariantStandalone,
@@ -180,6 +305,7 @@ export default {
:title="__('Labels')"
:loading="isLoading"
:can-edit="allowLabelEdit"
+ @open="oldIid = null"
>
<template #collapsed>
<dropdown-value
@@ -188,7 +314,7 @@ export default {
:allow-label-remove="allowLabelRemove"
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
- @onLabelRemove="$emit('onLabelRemove', $event)"
+ @onLabelRemove="handleLabelRemove"
>
<slot></slot>
</dropdown-value>
@@ -201,23 +327,25 @@ export default {
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
class="gl-mb-2"
- @onLabelRemove="$emit('onLabelRemove', $event)"
+ @onLabelRemove="handleLabelRemove"
>
<slot></slot>
</dropdown-value>
<dropdown-contents
+ ref="dropdownContents"
:dropdown-button-text="dropdownButtonText"
:allow-multiselect="allowMultiselect"
:labels-list-title="labelsListTitle"
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
:labels-create-title="labelsCreateTitle"
- :selected-labels="selectedLabels"
+ :selected-labels="issuableLabels"
:variant="variant"
- :issuable-type="issuableType"
:is-visible="edit"
:full-path="fullPath"
+ :workspace-type="workspaceType"
:attr-workspace-path="attrWorkspacePath"
+ :label-create-type="labelCreateType"
@setLabels="handleDropdownClose"
@closeDropdown="collapseEditableItem"
/>
@@ -233,10 +361,12 @@ export default {
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
:labels-create-title="labelsCreateTitle"
- :selected-labels="selectedLabels"
+ :selected-labels="issuableLabels"
:variant="variant"
- :issuable-type="issuableType"
:full-path="fullPath"
+ :workspace-type="workspaceType"
+ :attr-workspace-path="attrWorkspacePath"
+ :label-create-type="labelCreateType"
@setLabels="handleDropdownClose"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
index a2990d7171b..dffcc053fac 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
@@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/issuable_timelogs.fragment.graphql"
-query timeTrackingReport($id: IssueID!) {
+query issueTimeTrackingReport($id: IssueID!) {
issuable: issue(id: $id) {
__typename
id
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
index 753f1b345e3..ede9b75d765 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
@@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/issuable_timelogs.fragment.graphql"
-query timeTrackingReport($id: MergeRequestID!) {
+query mrTimeTrackingReport($id: MergeRequestID!) {
issuable: mergeRequest(id: $id) {
__typename
id
diff --git a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue
index 1eea660d527..a16dcb6d893 100644
--- a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue
+++ b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue
@@ -5,10 +5,10 @@ import { OBSTACLE_TYPES } from './constants';
const OBSTACLE_TEXT = {
[OBSTACLE_TYPES.oncallSchedules]: s__(
- 'OnCallSchedules|On-call schedule %{obstacle} in Project %{project}',
+ 'OnCallSchedules|On-call schedule %{obstacle} in project %{project}',
),
[OBSTACLE_TYPES.escalationPolicies]: s__(
- 'EscalationPolicies|Escalation policy %{obstacle} in Project %{project}',
+ 'EscalationPolicies|Escalation policy %{obstacle} in project %{project}',
),
};
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index 33fac5ebdbb..9cb66f6e65f 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -63,3 +63,6 @@ export const timeRanges = [
export const defaultTimeRange = timeRanges.find((tr) => tr.default);
export const getTimeWindow = (timeWindowName) =>
timeRanges.find((tr) => tr.name === timeWindowName);
+
+export const AVATAR_SHAPE_OPTION_CIRCLE = 'circle';
+export const AVATAR_SHAPE_OPTION_RECT = 'rect';
diff --git a/app/assets/javascripts/vue_shared/directives/validation.js b/app/assets/javascripts/vue_shared/directives/validation.js
index 779b04dc2bd..fc0ff78e7b4 100644
--- a/app/assets/javascripts/vue_shared/directives/validation.js
+++ b/app/assets/javascripts/vue_shared/directives/validation.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { __ } from '~/locale';
/**
* Validation messages will take priority based on the property order.
@@ -12,11 +12,11 @@ import { s__ } from '~/locale';
const defaultFeedbackMap = {
valueMissing: {
isInvalid: (el) => el.validity?.valueMissing,
- message: s__('Please fill out this field.'),
+ message: __('Please fill out this field.'),
},
urlTypeMismatch: {
isInvalid: (el) => el.type === 'url' && el.validity?.typeMismatch,
- message: s__('Please enter a valid URL format, ex: http://www.example.com/home'),
+ message: __('Please enter a valid URL format, ex: http://www.example.com/home'),
},
};
diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
index c1e8376d656..114f60c96ee 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
@@ -82,7 +82,7 @@ export default {
});
this.$root.$on('clicked::link', (e) => {
- window.location = e.target.href;
+ window.location = e.currentTarget.href;
});
},
diff --git a/app/assets/javascripts/vue_shared/security_configuration/provider.js b/app/assets/javascripts/vue_shared/security_configuration/provider.js
index fa23669b615..ef96b443da8 100644
--- a/app/assets/javascripts/vue_shared/security_configuration/provider.js
+++ b/app/assets/javascripts/vue_shared/security_configuration/provider.js
@@ -5,5 +5,5 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export default new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
index 3a4453bc7ae..e0669b3ed27 100644
--- a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
@@ -26,6 +26,11 @@ export default {
type: Number,
required: true,
},
+ injectedArtifacts: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -56,6 +61,9 @@ export default {
isLoadingReportArtifacts() {
return this.$apollo.queries.reportArtifacts.loading;
},
+ mergedReportArtifacts() {
+ return [...this.reportArtifacts, ...this.injectedArtifacts];
+ },
},
methods: {
showError(error) {
@@ -77,7 +85,7 @@ export default {
<template>
<security-report-download-dropdown
:title="s__('SecurityReports|Download results')"
- :artifacts="reportArtifacts"
+ :artifacts="mergedReportArtifacts"
:loading="isLoadingReportArtifacts"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js
index b024e92bd0e..fafbd02634f 100644
--- a/app/assets/javascripts/vue_shared/security_reports/constants.js
+++ b/app/assets/javascripts/vue_shared/security_reports/constants.js
@@ -17,6 +17,7 @@ export const REPORT_FILE_TYPES = {
* Security scan report types, as provided by the backend.
*/
export const REPORT_TYPE_SAST = 'sast';
+export const REPORT_TYPE_SAST_IAC = 'sast_iac';
export const REPORT_TYPE_DAST = 'dast';
export const REPORT_TYPE_DAST_PROFILES = 'dast_profiles';
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
diff --git a/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql
index b5858ab012b..e1f3c55a886 100644
--- a/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql
+++ b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql
@@ -1,6 +1,6 @@
#import "../fragments/job_artifacts.fragment.graphql"
-query getCorpuses($projectPath: ID!, $iid: ID, $reportTypes: [SecurityReportTypeEnum!]) {
+query getPipelineCorpuses($projectPath: ID!, $iid: ID, $reportTypes: [SecurityReportTypeEnum!]) {
project(fullPath: $projectPath) {
pipeline(iid: $iid) {
id
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
index 62a51abe038..8aefc13a5fa 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
@@ -18,6 +18,7 @@ export const fetchDiff = ({ state, rootState, dispatch }) => {
return fetchDiffData(rootState, state.paths.diffEndpoint, REPORT_TYPE_SAST)
.then((data) => {
dispatch('receiveDiffSuccess', data);
+ return data;
})
.catch(() => {
dispatch('receiveDiffError');
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js
index 722dcce3075..13ca154bfa7 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js
@@ -18,6 +18,7 @@ export const fetchDiff = ({ state, rootState, dispatch }) => {
return fetchDiffData(rootState, state.paths.diffEndpoint, REPORT_TYPE_SECRET_DETECTION)
.then((data) => {
dispatch('receiveDiffSuccess', data);
+ return data;
})
.catch(() => {
dispatch('receiveDiffError');
diff --git a/app/assets/javascripts/work_items/components/app.vue b/app/assets/javascripts/work_items/components/app.vue
index 93de17d1e43..a14d0c32cbe 100644
--- a/app/assets/javascripts/work_items/components/app.vue
+++ b/app/assets/javascripts/work_items/components/app.vue
@@ -1,9 +1,5 @@
-<script>
-export default {
- name: 'WorkItemRoot',
-};
-</script>
-
<template>
- <div></div>
+ <div>
+ <router-view />
+ </div>
</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
new file mode 100644
index 00000000000..b39f68abf74
--- /dev/null
+++ b/app/assets/javascripts/work_items/constants.js
@@ -0,0 +1,3 @@
+export const widgetTypes = {
+ title: 'TITLE',
+};
diff --git a/app/assets/javascripts/work_items/graphql/fragmentTypes.json b/app/assets/javascripts/work_items/graphql/fragmentTypes.json
new file mode 100644
index 00000000000..c048ac34ac0
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/fragmentTypes.json
@@ -0,0 +1 @@
+{"__schema":{"types":[{"kind":"INTERFACE","name":"WorkItemWidget","possibleTypes":[{"name":"TitleWidget"}]}]}}
diff --git a/app/assets/javascripts/work_items/graphql/provider.js b/app/assets/javascripts/work_items/graphql/provider.js
new file mode 100644
index 00000000000..083735336ce
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/provider.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
+import createDefaultClient from '~/lib/graphql';
+import workItemQuery from './work_item.query.graphql';
+import introspectionQueryResultData from './fragmentTypes.json';
+import typeDefs from './typedefs.graphql';
+
+const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData,
+});
+
+export function createApolloProvider() {
+ Vue.use(VueApollo);
+
+ const defaultClient = createDefaultClient(
+ {},
+ {
+ cacheConfig: {
+ fragmentMatcher,
+ },
+ typeDefs,
+ },
+ );
+
+ defaultClient.cache.writeQuery({
+ query: workItemQuery,
+ variables: {
+ id: '1',
+ },
+ data: {
+ workItem: {
+ __typename: 'WorkItem',
+ id: '1',
+ type: 'FEATURE',
+ widgets: {
+ __typename: 'WorkItemWidgetConnection',
+ nodes: [
+ {
+ __typename: 'TitleWidget',
+ type: 'TITLE',
+ enabled: true,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ contentText: 'Test Work Item Title',
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ return new VueApollo({
+ defaultClient,
+ });
+}
diff --git a/app/assets/javascripts/work_items/graphql/resolvers.js b/app/assets/javascripts/work_items/graphql/resolvers.js
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/resolvers.js
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index e69de29bb2d..4a6e4aeed60 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -0,0 +1,38 @@
+enum WorkItemType {
+ FEATURE
+}
+
+enum WidgetType {
+ TITLE
+}
+
+interface WorkItemWidget {
+ type: WidgetType!
+}
+
+# Replicating Relay connection type for client schema
+type WorkItemWidgetEdge {
+ cursor: String!
+ node: WorkItemWidget
+}
+
+type WorkItemWidgetConnection {
+ edges: [WorkItemWidgetEdge]
+ nodes: [WorkItemWidget]
+ pageInfo: PageInfo!
+}
+
+type TitleWidget implements WorkItemWidget {
+ type: WidgetType!
+ contentText: String!
+}
+
+type WorkItem {
+ id: ID!
+ type: WorkItemType!
+ widgets: [WorkItemWidgetConnection]
+}
+
+extend type Query {
+ workItem(id: ID!): WorkItem!
+}
diff --git a/app/assets/javascripts/work_items/graphql/widget.fragment.graphql b/app/assets/javascripts/work_items/graphql/widget.fragment.graphql
new file mode 100644
index 00000000000..d7608c26052
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/widget.fragment.graphql
@@ -0,0 +1,3 @@
+fragment WidgetBase on WorkItemWidget {
+ type
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
new file mode 100644
index 00000000000..549e4f8c65a
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -0,0 +1,16 @@
+#import './widget.fragment.graphql'
+
+query WorkItem($id: ID!) {
+ workItem(id: $id) @client {
+ id
+ type
+ widgets {
+ nodes {
+ ...WidgetBase
+ ... on TitleWidget {
+ contentText
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index a635d43776d..7cc8a23b7b1 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -1,11 +1,15 @@
import Vue from 'vue';
import App from './components/app.vue';
+import { createRouter } from './router';
+import { createApolloProvider } from './graphql/provider';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
return new Vue({
el,
+ router: createRouter(el.dataset.fullPath),
+ apolloProvider: createApolloProvider(),
render(createElement) {
return createElement(App);
},
diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue
new file mode 100644
index 00000000000..493ee0aba01
--- /dev/null
+++ b/app/assets/javascripts/work_items/pages/work_item_root.vue
@@ -0,0 +1,48 @@
+<script>
+import workItemQuery from '../graphql/work_item.query.graphql';
+import { widgetTypes } from '../constants';
+
+export default {
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ workItem: null,
+ };
+ },
+ apollo: {
+ workItem: {
+ query: workItemQuery,
+ variables() {
+ return {
+ id: this.id,
+ };
+ },
+ },
+ },
+ computed: {
+ titleWidgetData() {
+ return this.workItem?.widgets?.nodes?.find((widget) => widget.type === widgetTypes.title);
+ },
+ },
+};
+</script>
+
+<template>
+ <section>
+ <!-- Title widget placeholder -->
+ <div>
+ <h2
+ v-if="titleWidgetData"
+ class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5"
+ data-testid="title"
+ >
+ {{ titleWidgetData.contentText }}
+ </h2>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/work_items/router/index.js b/app/assets/javascripts/work_items/router/index.js
new file mode 100644
index 00000000000..142fab8cfa6
--- /dev/null
+++ b/app/assets/javascripts/work_items/router/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import { joinPaths } from '~/lib/utils/url_utility';
+import { routes } from './routes';
+
+Vue.use(VueRouter);
+
+export function createRouter(fullPath) {
+ return new VueRouter({
+ routes,
+ mode: 'history',
+ base: joinPaths(fullPath, '-', 'work_items'),
+ });
+}
diff --git a/app/assets/javascripts/work_items/router/routes.js b/app/assets/javascripts/work_items/router/routes.js
new file mode 100644
index 00000000000..a3cf44ad4ca
--- /dev/null
+++ b/app/assets/javascripts/work_items/router/routes.js
@@ -0,0 +1,8 @@
+export const routes = [
+ {
+ path: '/:id',
+ name: 'work_item',
+ component: () => import('../pages/work_item_root.vue'),
+ props: true,
+ },
+];
diff --git a/app/assets/stylesheets/emoji_sprites.scss b/app/assets/stylesheets/emoji_sprites.scss
index 01d13b30d2b..5a5f39a4b77 100644
--- a/app/assets/stylesheets/emoji_sprites.scss
+++ b/app/assets/stylesheets/emoji_sprites.scss
@@ -7,111 +7,111 @@
background-position: -20px 0;
}
-.emoji-1F627 {
+.emoji-8ball {
background-position: 0 -20px;
}
-.emoji-8ball {
+.emoji-a {
background-position: -20px -20px;
}
-.emoji-a {
+.emoji-ab {
background-position: -40px 0;
}
-.emoji-ab {
+.emoji-abc {
background-position: -40px -20px;
}
-.emoji-abc {
+.emoji-abcd {
background-position: 0 -40px;
}
-.emoji-abcd {
+.emoji-accept {
background-position: -20px -40px;
}
-.emoji-accept {
+.emoji-aerial_tramway {
background-position: -40px -40px;
}
-.emoji-aerial_tramway {
+.emoji-airplane {
background-position: -60px 0;
}
-.emoji-airplane {
+.emoji-airplane_arriving {
background-position: -60px -20px;
}
-.emoji-airplane_arriving {
+.emoji-airplane_departure {
background-position: -60px -40px;
}
-.emoji-airplane_departure {
+.emoji-airplane_small {
background-position: 0 -60px;
}
-.emoji-airplane_small {
+.emoji-alarm_clock {
background-position: -20px -60px;
}
-.emoji-alarm_clock {
+.emoji-alembic {
background-position: -40px -60px;
}
-.emoji-alembic {
+.emoji-alien {
background-position: -60px -60px;
}
-.emoji-alien {
+.emoji-ambulance {
background-position: -80px 0;
}
-.emoji-ambulance {
+.emoji-amphora {
background-position: -80px -20px;
}
-.emoji-amphora {
+.emoji-anchor {
background-position: -80px -40px;
}
-.emoji-anchor {
+.emoji-angel {
background-position: -80px -60px;
}
-.emoji-angel {
+.emoji-angel_tone1 {
background-position: 0 -80px;
}
-.emoji-angel_tone1 {
+.emoji-angel_tone2 {
background-position: -20px -80px;
}
-.emoji-angel_tone2 {
+.emoji-angel_tone3 {
background-position: -40px -80px;
}
-.emoji-angel_tone3 {
+.emoji-angel_tone4 {
background-position: -60px -80px;
}
-.emoji-angel_tone4 {
+.emoji-angel_tone5 {
background-position: -80px -80px;
}
-.emoji-angel_tone5 {
+.emoji-anger {
background-position: -100px 0;
}
-.emoji-anger {
+.emoji-anger_right {
background-position: -100px -20px;
}
-.emoji-anger_right {
+.emoji-angry {
background-position: -100px -40px;
}
-.emoji-angry {
+.emoji-anguished {
background-position: -100px -60px;
}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 06a8694eb3d..c1c8bfffff7 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -62,7 +62,6 @@
@import 'framework/sortable';
@import 'framework/ci_variable_list';
@import 'framework/feature_highlight';
-@import 'framework/terms';
@import 'framework/read_more';
@import 'framework/flex_grid';
@import 'framework/system_messages';
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index df78543f96d..8f65f349cf9 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -146,13 +146,6 @@
* Blame file
*/
&.blame {
- //
- // IMPORTANT PERFORMANCE OPTIMIZATION
- //
- // When viewinng a blame with many commits a lot of content is rendered on the page.
- // The line below ensures that we only render what is visible to the user, thus reducing TBT in the browser.
- content-visibility: auto;
-
table {
border: 0;
margin: 0;
@@ -167,12 +160,6 @@
}
td {
- //
- // IMPORTANT PERFORMANCE OPTIMIZATION
- //
- // When viewinng a blame with many commits a lot of content is rendered on the page.
- // The line below ensures that we only render what is visible to the user, thus reducing TBT in the browser.
- content-visibility: auto;
border-top: 0;
border-bottom: 0;
@@ -235,6 +222,25 @@
color: $gray-900;
}
}
+
+ //
+ // IMPORTANT PERFORMANCE OPTIMIZATION
+ //
+ // When viewinng a blame with many commits a lot of content is rendered on the page.
+ // content-visibility rules below ensure that we only render what is visible to the user, thus reducing TBT in the browser.
+ .commit {
+ content-visibility: auto;
+ contain-intrinsic-size: 1px 3em;
+ }
+
+ code .line {
+ content-visibility: auto;
+ contain-intrinsic-size: 1px 1.1875rem;
+ }
+
+ .line-numbers {
+ content-visibility: auto;
+ }
}
&.logs {
diff --git a/app/assets/stylesheets/framework/kbd.scss b/app/assets/stylesheets/framework/kbd.scss
index 05991bc16fd..7dd0ae47834 100644
--- a/app/assets/stylesheets/framework/kbd.scss
+++ b/app/assets/stylesheets/framework/kbd.scss
@@ -12,4 +12,20 @@ kbd {
border-image: none;
border-radius: 3px;
box-shadow: 0 -1px 0 var(--gray-200, $gray-200) inset;
+
+ &.flat {
+ color: $code-color;
+ background-color: $gray-100;
+ border-color: var(--gray-10, $gray-10) var(--gray-10, $gray-10) var(--gray-50, $gray-50);
+ box-shadow: none;
+ border-radius: $border-radius-default;
+ font-family: $monospace-font;
+ font-size: $gl-font-size-small;
+ line-height: 1;
+ white-space: pre-wrap;
+ // Safari
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ word-break: keep-all;
+ }
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index ef294635641..9b04b9a2612 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -82,11 +82,11 @@
.nav-links {
li.md-header-toolbar {
margin-left: auto;
- display: none;
+ visibility: hidden;
padding-bottom: $gl-padding-8;
&.active {
- display: block;
+ visibility: visible;
@include media-breakpoint-down(xs) {
flex: none;
@@ -116,7 +116,7 @@
}
.md-preview-holder {
- min-height: 167px;
+ min-height: 172px;
padding: 10px 0;
overflow-x: auto;
}
diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss
deleted file mode 100644
index b07d6023127..00000000000
--- a/app/assets/stylesheets/framework/terms.scss
+++ /dev/null
@@ -1,60 +0,0 @@
-.terms {
- .with-performance-bar & {
- margin-top: 0;
- }
-
- .alert-wrapper {
- min-height: $header-height + $gl-padding;
- }
-
- .content {
- padding-top: $gl-padding;
- }
-
- .card {
- .card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- line-height: $line-height-base;
-
- .logo-text {
- width: 55px;
- height: 24px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- }
-
- .navbar-collapse {
- padding-right: 0;
- flex-grow: 0;
- flex-basis: auto;
-
- .navbar-nav {
- margin: 0;
- }
- }
-
- .nav li {
- float: none;
- }
- }
-
- .panel-content {
- padding: $gl-padding;
-
- *:first-child {
- margin-top: 0;
- }
-
- *:last-child {
- margin-bottom: 0;
- }
- }
-
- .footer-block {
- margin: 0;
- }
- }
-}
diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss
index 8270db9966e..fb4266a2f41 100644
--- a/app/assets/stylesheets/highlight/common.scss
+++ b/app/assets/stylesheets/highlight/common.scss
@@ -96,9 +96,10 @@
}
@mixin line-number-link($color) {
+ min-width: $gl-spacing-scale-9;
+
&::before {
- @include gl-visibility-hidden;
- @include gl-display-inline-block;
+ @include gl-display-none;
@include gl-align-self-center;
@include gl-mt-2;
@include gl-mr-2;
@@ -114,10 +115,10 @@
}
&:hover::before {
- @include gl-visibility-visible;
+ @include gl-display-inline-block;
}
&:focus::before {
- @include gl-visibility-visible;
+ @include gl-display-inline-block;
}
}
diff --git a/app/assets/stylesheets/mailer.scss b/app/assets/stylesheets/mailer.scss
index 3220510775c..5f50489555b 100644
--- a/app/assets/stylesheets/mailer.scss
+++ b/app/assets/stylesheets/mailer.scss
@@ -143,6 +143,10 @@ table.content {
line-height: 1.4;
padding: 15px 5px;
text-align: center;
+
+ ul.list-style-position-inside {
+ list-style-position: inside;
+ }
}
td.mailer-align-left {
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index a3ec2167b13..d4c59a6ab0c 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -470,6 +470,10 @@
.labels-select-wrapper.is-embedded .labels-select-wrapper.is-embedded {
width: auto;
}
+
+ .show.dropdown .dropdown-menu {
+ @include gl-w-full;
+ }
}
.board-header-collapsed-info-icon:hover {
diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss
index 4beb5edbe7b..9fe0490571e 100644
--- a/app/assets/stylesheets/page_bundles/jira_connect.scss
+++ b/app/assets/stylesheets/page_bundles/jira_connect.scss
@@ -42,8 +42,6 @@ $header-height: 40px;
.jira-connect-app-body {
max-width: 768px;
- margin-left: auto;
- margin-right: auto;
}
// needed for external_link
diff --git a/app/assets/stylesheets/page_bundles/terms.scss b/app/assets/stylesheets/page_bundles/terms.scss
new file mode 100644
index 00000000000..8eb66e58aed
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/terms.scss
@@ -0,0 +1,64 @@
+@import 'mixins_and_variables_and_functions';
+
+.terms {
+ .with-system-header &,
+ .with-system-header.with-performance-bar &,
+ .with-performance-bar & {
+ margin-top: 0;
+ }
+
+ .terms-fade {
+ background: linear-gradient(0deg, $white 0%, rgba($white, 0.5) 100%);
+ }
+
+ .content {
+ padding-top: $gl-padding;
+ }
+
+ .gl-card {
+ .gl-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ line-height: $line-height-base;
+
+ .logo-text {
+ width: 55px;
+ height: 24px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ .navbar-collapse {
+ padding-right: 0;
+ flex-grow: 0;
+ flex-basis: auto;
+
+ .navbar-nav {
+ margin: 0;
+ }
+ }
+
+ .nav li {
+ float: none;
+ }
+ }
+
+ .panel-content {
+ padding: $gl-padding;
+
+ *:first-child {
+ margin-top: 0;
+ }
+
+ *:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .footer-block {
+ margin: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index de27ca2e5e8..b450bca4f41 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -1,9 +1,13 @@
.clusters-container {
- .empty-state .svg-content img {
- width: 145px;
+ .empty-state .svg-content {
+ @include gl-pb-0;
+
+ img {
+ width: 100px;
+ }
}
- .empty-state--agent {
+ .agents-empty-state {
.text-content {
@include gl-max-w-full;
@include media-breakpoint-up(lg) {
@@ -16,4 +20,23 @@
@include gl-flex-wrap;
}
}
+
+ .gl-card-body {
+ @include media-breakpoint-up(sm) {
+ @include gl-pt-2;
+ min-height: 372px;
+ }
+ }
+
+ @include media-breakpoint-down(xs) {
+ .nav-controls {
+ @include gl-w-full;
+ order: -1;
+
+ .gl-new-dropdown,
+ .split-content-button {
+ @include gl-w-full;
+ }
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/deploy_keys.scss b/app/assets/stylesheets/pages/deploy_keys.scss
index 2fafe052106..997e42a8fd5 100644
--- a/app/assets/stylesheets/pages/deploy_keys.scss
+++ b/app/assets/stylesheets/pages/deploy_keys.scss
@@ -1,12 +1,3 @@
-.deploy-keys-list {
- width: 100%;
- overflow: auto;
-
- table {
- border: 1px solid $table-border-color;
- }
-}
-
.deploy-keys-title {
padding-bottom: 2px;
line-height: 2;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index c597d2dd8da..cf5e93e94a2 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -179,6 +179,7 @@
}
.block,
+ .sidebar-contained-width,
.issuable-sidebar-header {
@include clearfix;
padding: $gl-padding 0;
@@ -317,6 +318,7 @@
padding: 0;
.block,
+ .sidebar-contained-width,
.issuable-sidebar-header {
width: $gutter-collapsed-width - 2px;
padding: 0;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index cec8d8a29cc..3b86750c6ca 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -269,7 +269,9 @@ $tabs-holder-z-index: 250;
}
.mr-widget-body {
- line-height: 28px;
+ &:not(.mr-widget-body-line-height-1) {
+ line-height: 28px;
+ }
@include clearfix;
diff --git a/app/assets/stylesheets/startup/_cloaking.scss b/app/assets/stylesheets/startup/_cloaking.scss
index 3c25feb0c5c..f60d72a51fb 100644
--- a/app/assets/stylesheets/startup/_cloaking.scss
+++ b/app/assets/stylesheets/startup/_cloaking.scss
@@ -2,6 +2,8 @@
Prevent flashing of content when using startup.css
*/
@mixin cloak-startup-scss($display) {
+ // General selector for cloaking until ready
+ .cloak-startup,
// Breadcrumbs and alerts on the top of the page
.content-wrapper > .alert-wrapper,
// Content on pages
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index d436c328921..efa4b04ee62 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -5,6 +5,7 @@
body.gl-dark {
--gray-50: #303030;
--gray-100: #404040;
+ --gray-600: #bfbfbf;
--gray-900: #fafafa;
--gray-950: #fff;
--green-100: #0d532a;
@@ -12,6 +13,7 @@ body.gl-dark {
--green-700: #91d4a8;
--blue-400: #1f75cb;
--orange-400: #ab6100;
+ --indigo-900-alpha-008: rgba(235, 235, 250, 0.08);
--gl-text-color: #fafafa;
--border-color: #4f4f4f;
--black: #fff;
@@ -1693,9 +1695,15 @@ body.gl-dark {
--black: #fff;
--svg-status-bg: #333;
}
+.nav-sidebar li a {
+ color: var(--gray-600);
+}
.nav-sidebar li.active {
box-shadow: none;
}
+.nav-sidebar li.active:not(.fly-out-top-item) > a:not(.has-sub-items) {
+ background-color: var(--indigo-900-alpha-008);
+}
body.gl-dark .navbar-gitlab {
background-color: #fafafa;
}
@@ -1780,7 +1788,7 @@ body.gl-dark .search .search-input-wrap .clear-icon {
fill: rgba(250, 250, 250, 0.8);
}
body.gl-dark .nav-sidebar li.active > a {
- color: #f0f0f0;
+ color: #fafafa;
}
body.gl-dark .nav-sidebar .fly-out-top-item a,
body.gl-dark .nav-sidebar .fly-out-top-item.active a,
@@ -1935,7 +1943,7 @@ body.gl-dark {
.gl-display-none {
display: none;
}
-@media (min-width: 36rem) {
+@media (min-width: 576px) {
.gl-sm-display-block {
display: block;
}
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index 40026c95a15..977f994dc78 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -1621,7 +1621,7 @@ svg.s16 {
.gl-display-none {
display: none;
}
-@media (min-width: 36rem) {
+@media (min-width: 576px) {
.gl-sm-display-block {
display: block;
}
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index 8d7531d6c9c..3daeeb30082 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -776,7 +776,7 @@ svg {
.gl-mb-5 {
margin-bottom: 1rem;
}
-@media (min-width: 36rem) {
+@media (min-width: 576px) {
.gl-sm-mt-0 {
margin-top: 0;
}
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index b77048174c9..2b5751cab36 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -42,8 +42,24 @@
}
.nav-sidebar {
- li.active {
- box-shadow: none;
+ li {
+ a {
+ color: var(--gray-600);
+ }
+
+ > a:hover {
+ background-color: var(--indigo-900-alpha-008);
+ }
+
+ &.active {
+ box-shadow: none;
+
+ &:not(.fly-out-top-item) {
+ > a:not(.has-sub-items) {
+ background-color: var(--indigo-900-alpha-008);
+ }
+ }
+ }
}
.sidebar-sub-level-items.fly-out-list {
@@ -53,7 +69,7 @@
}
body.gl-dark {
- @include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-800, $gray-900, $white);
+ @include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-900, $gray-900, $white);
.logo-text svg {
fill: var(--gl-text-color);
diff --git a/app/assets/stylesheets/themes/theme_blue.scss b/app/assets/stylesheets/themes/theme_blue.scss
index 9f9802f77f4..817557f37cd 100644
--- a/app/assets/stylesheets/themes/theme_blue.scss
+++ b/app/assets/stylesheets/themes/theme_blue.scss
@@ -6,7 +6,7 @@ body {
$theme-blue-200,
$theme-blue-500,
$theme-blue-700,
- $theme-blue-800,
+ $gray-900,
$theme-blue-900,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_dark.scss b/app/assets/stylesheets/themes/theme_dark.scss
index e6db6cd2a5e..4c52cdc30df 100644
--- a/app/assets/stylesheets/themes/theme_dark.scss
+++ b/app/assets/stylesheets/themes/theme_dark.scss
@@ -6,7 +6,7 @@ body {
$gray-200,
$gray-300,
$gray-500,
- $gray-700,
+ $gray-900,
$gray-900,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_green.scss b/app/assets/stylesheets/themes/theme_green.scss
index 6dcad6e1301..7e387e97452 100644
--- a/app/assets/stylesheets/themes/theme_green.scss
+++ b/app/assets/stylesheets/themes/theme_green.scss
@@ -6,7 +6,7 @@ body {
$theme-green-200,
$theme-green-500,
$theme-green-700,
- $theme-green-800,
+ $gray-900,
$theme-green-900,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_indigo.scss b/app/assets/stylesheets/themes/theme_indigo.scss
index 9566c9c6004..3bf6cfea650 100644
--- a/app/assets/stylesheets/themes/theme_indigo.scss
+++ b/app/assets/stylesheets/themes/theme_indigo.scss
@@ -6,7 +6,7 @@ body {
$indigo-200,
$indigo-500,
$indigo-700,
- $purple-900,
+ $gray-900,
$indigo-900,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_light.scss b/app/assets/stylesheets/themes/theme_light.scss
index 4c3bc1b2298..f2fdd499781 100644
--- a/app/assets/stylesheets/themes/theme_light.scss
+++ b/app/assets/stylesheets/themes/theme_light.scss
@@ -6,7 +6,7 @@ body {
$gray-500,
$gray-700,
$gray-500,
- $gray-500,
+ $gray-900,
$gray-50,
$gray-500
);
diff --git a/app/assets/stylesheets/themes/theme_light_blue.scss b/app/assets/stylesheets/themes/theme_light_blue.scss
index 07d1c60a4c6..771a84911b3 100644
--- a/app/assets/stylesheets/themes/theme_light_blue.scss
+++ b/app/assets/stylesheets/themes/theme_light_blue.scss
@@ -6,7 +6,7 @@ body {
$theme-light-blue-200,
$theme-light-blue-500,
$theme-light-blue-500,
- $theme-light-blue-700,
+ $gray-900,
$theme-light-blue-700,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_light_green.scss b/app/assets/stylesheets/themes/theme_light_green.scss
index e122501b93c..8c991a7bfb3 100644
--- a/app/assets/stylesheets/themes/theme_light_green.scss
+++ b/app/assets/stylesheets/themes/theme_light_green.scss
@@ -6,7 +6,7 @@ body {
$theme-green-200,
$theme-green-500,
$theme-green-500,
- $theme-light-green-700,
+ $gray-900,
$theme-light-green-700,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_light_indigo.scss b/app/assets/stylesheets/themes/theme_light_indigo.scss
index 5b607238ed9..6c220e0459a 100644
--- a/app/assets/stylesheets/themes/theme_light_indigo.scss
+++ b/app/assets/stylesheets/themes/theme_light_indigo.scss
@@ -6,7 +6,7 @@ body {
$indigo-200,
$indigo-500,
$indigo-500,
- $indigo-700,
+ $gray-900,
$indigo-700,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_light_red.scss b/app/assets/stylesheets/themes/theme_light_red.scss
index fd3980183f3..e1a715293b4 100644
--- a/app/assets/stylesheets/themes/theme_light_red.scss
+++ b/app/assets/stylesheets/themes/theme_light_red.scss
@@ -6,7 +6,7 @@ body {
$theme-light-red-200,
$theme-light-red-500,
$theme-light-red-500,
- $theme-light-red-700,
+ $gray-900,
$theme-light-red-700,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_red.scss b/app/assets/stylesheets/themes/theme_red.scss
index fa5ecc09f50..19fd150727d 100644
--- a/app/assets/stylesheets/themes/theme_red.scss
+++ b/app/assets/stylesheets/themes/theme_red.scss
@@ -6,7 +6,7 @@ body {
$theme-red-200,
$theme-red-500,
$theme-red-700,
- $theme-red-800,
+ $gray-900,
$theme-red-900,
$white
);
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index ec70926b418..7e46f16e1d0 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -281,3 +281,19 @@ $gl-line-height-42: px-to-rem(42px);
display: none;
}
}
+
+// Will both be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465
+.gl-text-transparent {
+ color: transparent;
+}
+
+.gl-focus-ring-border-1-gray-900\! {
+ @include gl-focus($gl-border-size-1, $gray-900, true);
+}
+
+// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2476
+.gl-md-max-w-50p {
+ @include gl-media-breakpoint-up(md) {
+ max-width: 50%;
+ }
+}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8039fac02ec..8644d95b96c 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -98,7 +98,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
# Specs are in spec/requests/self_monitoring_project_spec.rb
def create_self_monitoring_project
- job_id = SelfMonitoringProjectCreateWorker.perform_async # rubocop:disable CodeReuse/Worker
+ job_id = SelfMonitoringProjectCreateWorker.with_status.perform_async # rubocop:disable CodeReuse/Worker
render status: :accepted, json: {
job_id: job_id,
@@ -137,7 +137,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
# Specs are in spec/requests/self_monitoring_project_spec.rb
def delete_self_monitoring_project
- job_id = SelfMonitoringProjectDeleteWorker.perform_async # rubocop:disable CodeReuse/Worker
+ job_id = SelfMonitoringProjectDeleteWorker.with_status.perform_async # rubocop:disable CodeReuse/Worker
render status: :accepted, json: {
job_id: job_id,
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index ce7d64336c8..83413afb6b7 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -15,6 +15,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
end
def show
+ @created = get_created_session
end
def new
@@ -33,6 +34,8 @@ class Admin::ApplicationsController < Admin::ApplicationController
if @application.persisted?
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
+ set_created_session
+
redirect_to admin_application_url(@application)
else
render :new
diff --git a/app/controllers/admin/hook_logs_controller.rb b/app/controllers/admin/hook_logs_controller.rb
index 444ad17f86d..aa13673095d 100644
--- a/app/controllers/admin/hook_logs_controller.rb
+++ b/app/controllers/admin/hook_logs_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::HookLogsController < Admin::ApplicationController
- include HooksExecution
+ include ::Integrations::HooksExecution
before_action :hook, only: [:show, :retry]
before_action :hook_log, only: [:show, :retry]
@@ -9,6 +9,7 @@ class Admin::HookLogsController < Admin::ApplicationController
respond_to :html
feature_category :integrations
+ urgency :low, [:retry]
def show
end
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index ca24f671b9d..6f5475a4a78 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -1,14 +1,15 @@
# frozen_string_literal: true
class Admin::HooksController < Admin::ApplicationController
- include HooksExecution
+ include ::Integrations::HooksExecution
before_action :hook_logs, only: :edit
feature_category :integrations
+ urgency :low, [:test]
def index
- @hooks = SystemHook.all
+ @hooks = SystemHook.all.load
@hook = SystemHook.new
end
diff --git a/app/controllers/admin/integrations_controller.rb b/app/controllers/admin/integrations_controller.rb
index a3eb24b9b6f..ad0ee0b2cef 100644
--- a/app/controllers/admin/integrations_controller.rb
+++ b/app/controllers/admin/integrations_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::IntegrationsController < Admin::ApplicationController
- include IntegrationsActions
+ include ::Integrations::Actions
before_action :not_found, unless: -> { instance_level_integrations? }
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index 6cc11b40de0..822b7a93c9c 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -3,7 +3,7 @@
class Admin::LabelsController < Admin::ApplicationController
before_action :set_label, only: [:show, :edit, :update, :destroy]
- feature_category :issue_tracking
+ feature_category :team_planning
def index
@labels = Label.templates.page(params[:page])
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 8c74352a179..9312651b8bf 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -8,7 +8,6 @@ class Admin::RunnersController < Admin::ApplicationController
feature_category :runner
def index
- @active_runners_count = Ci::Runner.online.count
end
def show
@@ -86,9 +85,12 @@ class Admin::RunnersController < Admin::ApplicationController
Project.all
end
- @projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any?
- @projects = @projects.inc_routes
- @projects = @projects.page(params[:page]).per(30).without_count
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
+ @projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any?
+ @projects = @projects.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659')
+ @projects = @projects.inc_routes
+ @projects = @projects.page(params[:page]).per(30).without_count
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b22167a3952..3af1afab06e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -163,7 +163,8 @@ class ApplicationController < ActionController::Base
payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
payload[:metadata] = @current_context
-
+ payload[:request_urgency] = urgency&.name
+ payload[:target_duration_s] = urgency&.duration
logged_user = auth_user
if logged_user.present?
payload[:user_id] = logged_user.try(:id)
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 1c07245da08..5cb5690d72d 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -5,7 +5,7 @@ class AutocompleteController < ApplicationController
feature_category :users, [:users, :user]
feature_category :projects, [:projects]
- feature_category :issue_tracking, [:award_emojis]
+ feature_category :team_planning, [:award_emojis]
feature_category :code_review, [:merge_request_target_branches]
feature_category :continuous_delivery, [:deploy_keys_with_owners]
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 7dea6191fa4..e7ae941886d 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -21,7 +21,7 @@ module Boards
before_action :validate_id_list, only: [:bulk_move]
before_action :can_move_issues?, only: [:bulk_move]
- feature_category :boards
+ feature_category :team_planning
def index
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
index 8ab8337a3ad..696b251301f 100644
--- a/app/controllers/boards/lists_controller.rb
+++ b/app/controllers/boards/lists_controller.rb
@@ -8,7 +8,7 @@ module Boards
before_action :authorize_read_list, only: [:index]
skip_before_action :authenticate_user!, only: [:index]
- feature_category :boards
+ feature_category :team_planning
def index
lists = Boards::Lists::ListService.new(board.resource_parent, current_user).execute(board)
diff --git a/app/controllers/concerns/gitlab_recaptcha.rb b/app/controllers/concerns/gitlab_recaptcha.rb
new file mode 100644
index 00000000000..15e856463ea
--- /dev/null
+++ b/app/controllers/concerns/gitlab_recaptcha.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module GitlabRecaptcha
+ extend ActiveSupport::Concern
+ include Recaptcha::Verify
+ include RecaptchaHelper
+
+ def load_recaptcha
+ recaptcha_enabled? && Gitlab::Recaptcha.load_configurations!
+ end
+
+ def check_recaptcha
+ return unless load_recaptcha
+ return if verify_recaptcha
+
+ flash[:alert] = _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
+ flash.delete :recaptcha_error
+
+ self.resource = resource_class.new
+ render action: 'new'
+ end
+end
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
index 35c1f358a77..e02a766d2b9 100644
--- a/app/controllers/concerns/group_tree.rb
+++ b/app/controllers/concerns/group_tree.rb
@@ -38,13 +38,7 @@ module GroupTree
#
# Pagination needs to be applied before loading the ancestors to
# make sure ancestors are not cut off by pagination.
- filtered_groups_relation = Group.where(id: filtered_groups.select(:id))
-
- if Feature.enabled?(:linear_group_tree_ancestor_scopes, current_user, default_enabled: :yaml)
- filtered_groups_relation.self_and_ancestors
- else
- Gitlab::ObjectHierarchy.new(filtered_groups_relation).base_and_ancestors
- end
+ Group.where(id: filtered_groups.select(:id)).self_and_ancestors
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/concerns/hooks_execution.rb b/app/controllers/concerns/hooks_execution.rb
deleted file mode 100644
index 87d215f50e7..00000000000
--- a/app/controllers/concerns/hooks_execution.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-module HooksExecution
- extend ActiveSupport::Concern
-
- private
-
- def destroy_hook(hook)
- result = WebHooks::DestroyService.new(current_user).execute(hook)
-
- if result[:status] == :success
- flash[:notice] =
- if result[:async]
- _("%{hook_type} was scheduled for deletion") % { hook_type: hook.model_name.human }
- else
- _("%{hook_type} was deleted") % { hook_type: hook.model_name.human }
- end
- else
- flash[:alert] = result[:message]
- end
- end
-
- def set_hook_execution_notice(result)
- http_status = result[:http_status]
- message = result[:message]
-
- if http_status && http_status >= 200 && http_status < 400
- flash[:notice] = "Hook executed successfully: HTTP #{http_status}"
- elsif http_status
- flash[:alert] = "Hook executed successfully but returned HTTP #{http_status} #{message}"
- else
- flash[:alert] = "Hook execution failed: #{message}"
- end
- end
-
- def create_rate_limit(key, scope)
- if rate_limiter.throttled?(key, scope: [scope, current_user])
- rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
-
- render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
- end
- end
-
- def rate_limiter
- ::Gitlab::ApplicationRateLimiter
- end
-end
diff --git a/app/controllers/concerns/integrations/actions.rb b/app/controllers/concerns/integrations/actions.rb
new file mode 100644
index 00000000000..6490742c0f8
--- /dev/null
+++ b/app/controllers/concerns/integrations/actions.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module Integrations::Actions
+ extend ActiveSupport::Concern
+
+ included do
+ include Integrations::Params
+ include IntegrationsHelper
+
+ before_action :integration, only: [:edit, :update, :overrides, :test]
+
+ urgency :low, [:test]
+ end
+
+ def edit
+ render 'shared/integrations/edit'
+ end
+
+ def update
+ saved = integration.update(integration_params[:integration])
+
+ respond_to do |format|
+ format.html do
+ if saved
+ PropagateIntegrationWorker.perform_async(integration.id)
+ redirect_to scoped_edit_integration_path(integration), notice: success_message
+ else
+ render 'shared/integrations/edit'
+ end
+ end
+
+ format.json do
+ status = saved ? :ok : :unprocessable_entity
+
+ render json: serialize_as_json, status: status
+ end
+ end
+ end
+
+ def test
+ render json: {}, status: :ok
+ end
+
+ def reset
+ integration.destroy!
+
+ flash[:notice] = s_('Integrations|This integration, and inheriting projects were reset.')
+
+ render json: {}, status: :ok
+ end
+
+ private
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def integration
+ @integration ||= find_or_initialize_non_project_specific_integration(params[:id])
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ def success_message
+ if integration.active?
+ s_('Integrations|%{integration} settings saved and active.') % { integration: integration.title }
+ else
+ s_('Integrations|%{integration} settings saved, but not active.') % { integration: integration.title }
+ end
+ end
+
+ def serialize_as_json
+ integration
+ .as_json(only: integration.json_fields)
+ .merge(errors: integration.errors.as_json)
+ end
+end
diff --git a/app/controllers/concerns/integrations/hooks_execution.rb b/app/controllers/concerns/integrations/hooks_execution.rb
new file mode 100644
index 00000000000..af039057a9c
--- /dev/null
+++ b/app/controllers/concerns/integrations/hooks_execution.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Integrations::HooksExecution
+ extend ActiveSupport::Concern
+
+ private
+
+ def destroy_hook(hook)
+ result = WebHooks::DestroyService.new(current_user).execute(hook)
+
+ if result[:status] == :success
+ flash[:notice] =
+ if result[:async]
+ _("%{hook_type} was scheduled for deletion") % { hook_type: hook.model_name.human }
+ else
+ _("%{hook_type} was deleted") % { hook_type: hook.model_name.human }
+ end
+ else
+ flash[:alert] = result[:message]
+ end
+ end
+
+ def set_hook_execution_notice(result)
+ http_status = result[:http_status]
+ message = result[:message]
+
+ if http_status && http_status >= 200 && http_status < 400
+ flash[:notice] = "Hook executed successfully: HTTP #{http_status}"
+ elsif http_status
+ flash[:alert] = "Hook executed successfully but returned HTTP #{http_status} #{message}"
+ else
+ flash[:alert] = "Hook execution failed: #{message}"
+ end
+ end
+
+ def create_rate_limit(key, scope)
+ if rate_limiter.throttled?(key, scope: [scope, current_user])
+ rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
+
+ render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
+ end
+ end
+
+ def rate_limiter
+ ::Gitlab::ApplicationRateLimiter
+ end
+end
diff --git a/app/controllers/concerns/integrations/params.rb b/app/controllers/concerns/integrations/params.rb
index 62585ab95af..201fb1dc83f 100644
--- a/app/controllers/concerns/integrations/params.rb
+++ b/app/controllers/concerns/integrations/params.rb
@@ -9,6 +9,7 @@ module Integrations
:add_pusher,
:alert_events,
:api_key,
+ :api_token,
:api_url,
:bamboo_url,
:branches_to_be_notified,
@@ -74,7 +75,8 @@ module Integrations
:url,
:user_key,
:username,
- :webhook
+ :webhook,
+ :zentao_product_xid
].freeze
def integration_params
diff --git a/app/controllers/concerns/integrations_actions.rb b/app/controllers/concerns/integrations_actions.rb
deleted file mode 100644
index dd066cc1b02..00000000000
--- a/app/controllers/concerns/integrations_actions.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-module IntegrationsActions
- extend ActiveSupport::Concern
-
- included do
- include Integrations::Params
- include IntegrationsHelper
-
- before_action :integration, only: [:edit, :update, :overrides, :test]
- end
-
- def edit
- render 'shared/integrations/edit'
- end
-
- def update
- saved = integration.update(integration_params[:integration])
-
- respond_to do |format|
- format.html do
- if saved
- PropagateIntegrationWorker.perform_async(integration.id)
- redirect_to scoped_edit_integration_path(integration), notice: success_message
- else
- render 'shared/integrations/edit'
- end
- end
-
- format.json do
- status = saved ? :ok : :unprocessable_entity
-
- render json: serialize_as_json, status: status
- end
- end
- end
-
- def test
- render json: {}, status: :ok
- end
-
- def reset
- integration.destroy!
-
- flash[:notice] = s_('Integrations|This integration, and inheriting projects were reset.')
-
- render json: {}, status: :ok
- end
-
- private
-
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
- def integration
- @integration ||= find_or_initialize_non_project_specific_integration(params[:id])
- end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
-
- def success_message
- if integration.active?
- s_('Integrations|%{integration} settings saved and active.') % { integration: integration.title }
- else
- s_('Integrations|%{integration} settings saved, but not active.') % { integration: integration.title }
- end
- end
-
- def serialize_as_json
- integration
- .as_json(only: integration.json_fields)
- .merge(errors: integration.errors.as_json)
- end
-end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index e1e662a1968..2d7fbb78209 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -149,8 +149,20 @@ module IssuableActions
.includes(:noteable)
.fresh
+ if paginated_discussions
+ paginated_discussions_by_type = paginated_discussions.records.group_by(&:table_name)
+
+ notes = if paginated_discussions_by_type['notes'].present?
+ notes.with_discussion_ids(paginated_discussions_by_type['notes'].map(&:discussion_id))
+ else
+ notes.none
+ end
+
+ response.headers['X-Next-Page-Cursor'] = paginated_discussions.cursor_for_next_page if paginated_discussions.has_next_page?
+ end
+
if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
- notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes)
+ notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user, paginated_notes: paginated_discussions_by_type).execute(notes)
end
notes = prepare_notes_for_rendering(notes)
@@ -159,9 +171,9 @@ module IssuableActions
discussions = Discussion.build_collection(notes, issuable)
if issuable.is_a?(MergeRequest)
- cache_context = [current_user&.cache_key, project.team.human_max_access(current_user&.id)].join(':')
-
- render_cached(discussions, with: discussion_serializer, cache_context: -> (_) { cache_context }, context: self)
+ render_cached(discussions, with: discussion_serializer, cache_context: -> (_) { discussion_cache_context }, context: self)
+ elsif issuable.is_a?(Issue)
+ render json: discussion_serializer.represent(discussions, context: self) if stale?(etag: [discussion_cache_context, discussions])
else
render json: discussion_serializer.represent(discussions, context: self)
end
@@ -170,6 +182,17 @@ module IssuableActions
private
+ def paginated_discussions
+ return if params[:per_page].blank?
+ return unless issuable.instance_of?(Issue) && Feature.enabled?(:paginated_issue_discussions, project, default_enabled: :yaml)
+
+ strong_memoize(:paginated_discussions) do
+ issuable
+ .discussion_root_note_ids(notes_filter: notes_filter)
+ .keyset_paginate(cursor: params[:cursor], per_page: params[:per_page].to_i)
+ end
+ end
+
def notes_filter
strong_memoize(:notes_filter) do
notes_filter_param = params[:notes_filter]&.to_i
@@ -197,6 +220,10 @@ module IssuableActions
current_user&.user_preference&.previous_changes&.any?
end
+ def discussion_cache_context
+ [current_user&.cache_key, project.team.human_max_access(current_user&.id)].join(':')
+ end
+
def discussion_serializer
DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity)
end
diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb
index d2c746db12d..794307ebb0c 100644
--- a/app/controllers/concerns/oauth_applications.rb
+++ b/app/controllers/concerns/oauth_applications.rb
@@ -3,6 +3,8 @@
module OauthApplications
extend ActiveSupport::Concern
+ CREATED_SESSION_KEY = :oauth_applications_created
+
included do
before_action :prepare_scopes, only: [:create, :update]
end
@@ -15,6 +17,14 @@ module OauthApplications
end
end
+ def set_created_session
+ session[CREATED_SESSION_KEY] = true
+ end
+
+ def get_created_session
+ session.delete(CREATED_SESSION_KEY) || false
+ end
+
def load_scopes
@scopes ||= Doorkeeper.configuration.scopes
end
diff --git a/app/controllers/concerns/one_trust_csp.rb b/app/controllers/concerns/one_trust_csp.rb
index 4e98ec586ca..fbd44f52590 100644
--- a/app/controllers/concerns/one_trust_csp.rb
+++ b/app/controllers/concerns/one_trust_csp.rb
@@ -5,7 +5,7 @@ module OneTrustCSP
included do
content_security_policy do |policy|
- next if policy.directives.blank?
+ next unless helpers.one_trust_enabled? || policy.directives.present?
default_script_src = policy.directives['script-src'] || policy.directives['default-src']
script_src_values = Array.wrap(default_script_src) | ["'unsafe-eval'", 'https://cdn.cookielaw.org https://*.onetrust.com']
diff --git a/app/controllers/concerns/workhorse_authorization.rb b/app/controllers/concerns/workhorse_authorization.rb
index a290ba256b6..f9b85944307 100644
--- a/app/controllers/concerns/workhorse_authorization.rb
+++ b/app/controllers/concerns/workhorse_authorization.rb
@@ -26,7 +26,7 @@ module WorkhorseAuthorization
def file_is_valid?(file)
return false unless file.is_a?(::UploadedFile)
- file_extension_whitelist.include?(File.extname(file.original_filename).downcase.delete('.'))
+ file_extension_allowlist.include?(File.extname(file.original_filename).downcase.delete('.'))
end
def uploader_class
@@ -37,7 +37,7 @@ module WorkhorseAuthorization
raise NotImplementedError
end
- def file_extension_whitelist
- ImportExportUploader::EXTENSION_WHITELIST
+ def file_extension_allowlist
+ ImportExportUploader::EXTENSION_ALLOWLIST
end
end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 0b833e149a4..6725e19df25 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -2,6 +2,10 @@
class ConfirmationsController < Devise::ConfirmationsController
include AcceptsPendingInvitations
+ include GitlabRecaptcha
+
+ prepend_before_action :check_recaptcha, only: :create
+ before_action :load_recaptcha, only: :new
feature_category :users
@@ -31,6 +35,12 @@ class ConfirmationsController < Devise::ConfirmationsController
end
end
+ def check_recaptcha
+ return unless resource_params[:email].present?
+
+ super
+ end
+
def after_sign_in(resource)
after_sign_in_path_for(resource)
end
diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb
index b661efa12c0..d2f31258ecd 100644
--- a/app/controllers/dashboard/labels_controller.rb
+++ b/app/controllers/dashboard/labels_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Dashboard::LabelsController < Dashboard::ApplicationController
- feature_category :issue_tracking
+ feature_category :team_planning
def index
respond_to do |format|
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 1369e82a69b..34d9739d91c 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -4,7 +4,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
before_action :projects
before_action :groups, only: :index
- feature_category :issue_tracking
+ feature_category :team_planning
def index
respond_to do |format|
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 21bbb4d0c98..2c5e6817427 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -8,7 +8,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
before_action :authorize_read_group!, only: :index
before_action :find_todos, only: [:index, :destroy_all]
- feature_category :issue_tracking
+ feature_category :team_planning
def index
@sort = params[:sort]
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 227dd0591d4..8d7686a95fb 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -15,7 +15,7 @@ class DashboardController < Dashboard::ApplicationController
respond_to :html
feature_category :users, [:activity]
- feature_category :issue_tracking, [:issues, :issues_calendar]
+ feature_category :team_planning, [:issues, :issues_calendar]
feature_category :code_review, [:merge_requests]
def activity
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 3dc6a16cbc1..14dd2ae5691 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -68,6 +68,20 @@ class Explore::ProjectsController < Explore::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
+ def topics
+ load_project_counts
+ load_topics
+ end
+
+ def topic
+ load_topic
+
+ return render_404 unless @topic
+
+ params[:topic] = @topic.name
+ @projects = load_projects
+ end
+
private
def load_project_counts
@@ -86,6 +100,14 @@ class Explore::ProjectsController < Explore::ApplicationController
prepare_projects_for_rendering(projects)
end
+ def load_topics
+ @topics = Projects::TopicsFinder.new(params: params.permit(:search)).execute.page(params[:page]).without_count
+ end
+
+ def load_topic
+ @topic = Projects::Topic.find_by_name(params[:topic_name])
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def preload_associations(projects)
projects.includes(:route, :creator, :group, :project_feature, :topics, namespace: [:route, :owner])
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 0722a712b5c..fde0f133e53 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -188,6 +188,5 @@ class GraphqlController < ApplicationController
def logs
RequestStore.store[:graphql_logs].to_a
- .map { |log| log.except(:duration_s, :query_string) }
end
end
diff --git a/app/controllers/groups/autocomplete_sources_controller.rb b/app/controllers/groups/autocomplete_sources_controller.rb
index 5270a718952..82f8854bd2b 100644
--- a/app/controllers/groups/autocomplete_sources_controller.rb
+++ b/app/controllers/groups/autocomplete_sources_controller.rb
@@ -2,7 +2,7 @@
class Groups::AutocompleteSourcesController < Groups::ApplicationController
feature_category :subgroups, [:members]
- feature_category :issue_tracking, [:issues, :labels, :milestones, :commands]
+ feature_category :team_planning, [:issues, :labels, :milestones, :commands]
feature_category :code_review, [:merge_requests]
def members
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index e8e6a7e5c1a..3152c4d733f 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -12,9 +12,13 @@ class Groups::BoardsController < Groups::ApplicationController
push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, group, default_enabled: :yaml)
+ experiment(:prominent_create_board_btn, subject: current_user) do |e|
+ e.use { }
+ e.try { }
+ end.run
end
- feature_category :boards
+ feature_category :team_planning
private
diff --git a/app/controllers/groups/crm_controller.rb b/app/controllers/groups/crm_controller.rb
new file mode 100644
index 00000000000..40661b09be6
--- /dev/null
+++ b/app/controllers/groups/crm_controller.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class Groups::CrmController < Groups::ApplicationController
+ feature_category :team_planning
+
+ before_action :authorize_read_crm_contact!, only: [:contacts]
+ before_action :authorize_read_crm_organization!, only: [:organizations]
+
+ def contacts
+ respond_to do |format|
+ format.html
+ end
+ end
+
+ def organizations
+ respond_to do |format|
+ format.html
+ end
+ end
+
+ private
+
+ def authorize_read_crm_contact!
+ render_404 unless can?(current_user, :read_crm_contact, group)
+ end
+
+ def authorize_read_crm_organization!
+ render_404 unless can?(current_user, :read_crm_organization, group)
+ end
+end
diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
index e19b8ae35f8..fc930ffebbd 100644
--- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb
+++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
@@ -11,8 +11,8 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
before_action :ensure_token_granted!, only: [:blob, :manifest]
before_action :ensure_feature_enabled!
- before_action :verify_workhorse_api!, only: [:authorize_upload_blob, :upload_blob]
- skip_before_action :verify_authenticity_token, only: [:authorize_upload_blob, :upload_blob]
+ before_action :verify_workhorse_api!, only: [:authorize_upload_blob, :upload_blob, :authorize_upload_manifest, :upload_manifest]
+ skip_before_action :verify_authenticity_token, only: [:authorize_upload_blob, :upload_blob, :authorize_upload_manifest, :upload_manifest]
attr_reader :token
@@ -22,20 +22,11 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
result = DependencyProxy::FindOrCreateManifestService.new(group, image, tag, token).execute
if result[:status] == :success
- response.headers['Docker-Content-Digest'] = result[:manifest].digest
- response.headers['Content-Length'] = result[:manifest].size
- response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION
- response.headers['Etag'] = "\"#{result[:manifest].digest}\""
- content_type = result[:manifest].content_type
-
- event_name = tracking_event_name(object_type: :manifest, from_cache: result[:from_cache])
- track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
- send_upload(
- result[:manifest].file,
- proxy: true,
- redirect_params: { query: { 'response-content-type' => content_type } },
- send_params: { type: content_type }
- )
+ if result[:manifest]
+ send_manifest(result[:manifest], from_cache: result[:from_cache])
+ else
+ send_dependency(manifest_header, DependencyProxy::Registry.manifest_url(image, tag), manifest_file_name)
+ end
else
render status: result[:http_status], json: result[:message]
end
@@ -59,7 +50,7 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
def authorize_upload_blob
set_workhorse_internal_api_content_type
- render json: DependencyProxy::FileUploader.workhorse_authorize(has_length: false)
+ render json: DependencyProxy::FileUploader.workhorse_authorize(has_length: false, maximum_size: DependencyProxy::Blob::MAX_FILE_SIZE)
end
def upload_blob
@@ -75,6 +66,37 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
head :ok
end
+ def authorize_upload_manifest
+ set_workhorse_internal_api_content_type
+
+ render json: DependencyProxy::FileUploader.workhorse_authorize(has_length: false, maximum_size: DependencyProxy::Manifest::MAX_FILE_SIZE)
+ end
+
+ def upload_manifest
+ attrs = {
+ file_name: manifest_file_name,
+ content_type: request.headers[Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER],
+ digest: request.headers[DependencyProxy::Manifest::DIGEST_HEADER],
+ file: params[:file],
+ size: params[:file].size
+ }
+
+ manifest = @group.dependency_proxy_manifests
+ .active
+ .find_by_file_name(manifest_file_name)
+
+ if manifest
+ manifest.update!(attrs)
+ else
+ @group.dependency_proxy_manifests.create!(attrs)
+ end
+
+ event_name = tracking_event_name(object_type: :manifest, from_cache: false)
+ track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
+
+ head :ok
+ end
+
private
def blob_via_workhorse
@@ -86,14 +108,36 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
send_upload(blob.file)
else
- send_dependency(token, DependencyProxy::Registry.blob_url(image, params[:sha]), blob_file_name)
+ send_dependency(token_header, DependencyProxy::Registry.blob_url(image, params[:sha]), blob_file_name)
end
end
+ def send_manifest(manifest, from_cache:)
+ response.headers[DependencyProxy::Manifest::DIGEST_HEADER] = manifest.digest
+ response.headers['Content-Length'] = manifest.size
+ response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION
+ response.headers['Etag'] = "\"#{manifest.digest}\""
+ content_type = manifest.content_type
+
+ event_name = tracking_event_name(object_type: :manifest, from_cache: from_cache)
+ track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
+
+ send_upload(
+ manifest.file,
+ proxy: true,
+ redirect_params: { query: { 'response-content-type' => content_type } },
+ send_params: { type: content_type }
+ )
+ end
+
def blob_file_name
@blob_file_name ||= params[:sha].sub('sha256:', '') + '.gz'
end
+ def manifest_file_name
+ @manifest_file_name ||= "#{image}:#{tag}.json"
+ end
+
def group
strong_memoize(:group) do
Group.find_by_full_path(params[:group_id], follow_redirects: true)
@@ -137,4 +181,12 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
render status: result[:http_status], json: result[:message]
end
end
+
+ def token_header
+ { Authorization: ["Bearer #{token}"] }
+ end
+
+ def manifest_header
+ token_header.merge(Accept: ::ContainerRegistry::Client::ACCEPTED_TYPES)
+ end
end
diff --git a/app/controllers/groups/email_campaigns_controller.rb b/app/controllers/groups/email_campaigns_controller.rb
index 70c8a23d918..520ad768939 100644
--- a/app/controllers/groups/email_campaigns_controller.rb
+++ b/app/controllers/groups/email_campaigns_controller.rb
@@ -40,7 +40,7 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
project_pipelines_url(group.projects.first)
when :trial, :trial_short
'https://about.gitlab.com/free-trial/'
- when :team, :team_short
+ when :team, :team_short, :invite_team
group_group_members_url(group)
when :admin_verify
project_settings_ci_cd_path(group.projects.first, ci_runner_templates: true, anchor: 'js-runners-settings')
@@ -59,6 +59,11 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
@track = params[:track]&.to_sym
@series = params[:series]&.to_i
+ # There is only one email that will be sent for invite team track so series
+ # should only have the value 0. Return early if track is invite team and
+ # condition for series value is met
+ return if @track == Namespaces::InviteTeamEmailService::TRACK && @series == 0
+
track_valid = @track.in?(Namespaces::InProductMarketingEmailsService::TRACKS.keys)
return render_404 unless track_valid
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 86dde454cbc..7bcc8182bd6 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -9,7 +9,7 @@ class Groups::LabelsController < Groups::ApplicationController
respond_to :html
- feature_category :issue_tracking
+ feature_category :team_planning
def index
respond_to do |format|
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 63eff750d1b..75877cdef9c 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -6,7 +6,7 @@ class Groups::MilestonesController < Groups::ApplicationController
before_action :milestone, only: [:edit, :show, :update, :issues, :merge_requests, :participants, :labels, :destroy]
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy]
- feature_category :issue_tracking
+ feature_category :team_planning
def index
respond_to do |format|
diff --git a/app/controllers/groups/packages_controller.rb b/app/controllers/groups/packages_controller.rb
index d02a8262948..47f1816cc4c 100644
--- a/app/controllers/groups/packages_controller.rb
+++ b/app/controllers/groups/packages_controller.rb
@@ -6,10 +6,6 @@ module Groups
feature_category :package_registry
- before_action do
- push_frontend_feature_flag(:package_list_apollo, default_enabled: :yaml)
- end
-
private
def verify_packages_enabled!
diff --git a/app/controllers/groups/settings/applications_controller.rb b/app/controllers/groups/settings/applications_controller.rb
index f05a96d7810..6388277e4dc 100644
--- a/app/controllers/groups/settings/applications_controller.rb
+++ b/app/controllers/groups/settings/applications_controller.rb
@@ -16,6 +16,7 @@ module Groups
end
def show
+ @created = get_created_session
end
def edit
@@ -27,6 +28,8 @@ module Groups
if @application.persisted?
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
+ set_created_session
+
redirect_to group_settings_application_url(@group, @application)
else
set_index_vars
diff --git a/app/controllers/groups/settings/integrations_controller.rb b/app/controllers/groups/settings/integrations_controller.rb
index a7a1de03224..0a63c3d304b 100644
--- a/app/controllers/groups/settings/integrations_controller.rb
+++ b/app/controllers/groups/settings/integrations_controller.rb
@@ -3,7 +3,7 @@
module Groups
module Settings
class IntegrationsController < Groups::ApplicationController
- include IntegrationsActions
+ include ::Integrations::Actions
before_action :authorize_admin_group!
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index a419171039e..6ae711a6e14 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -54,7 +54,7 @@ class GroupsController < Groups::ApplicationController
:destroy, :details, :transfer, :activity
]
- feature_category :issue_tracking, [:issues, :issues_calendar, :preview_markdown]
+ feature_category :team_planning, [:issues, :issues_calendar, :preview_markdown]
feature_category :code_review, [:merge_requests, :unfoldered_environment_names]
feature_category :projects, [:projects]
feature_category :importers, [:export, :download_export]
@@ -92,6 +92,7 @@ class GroupsController < Groups::ApplicationController
if @group.import_state&.in_progress?
redirect_to group_import_path(@group)
else
+ publish_invite_members_for_task_experiment
render_show_html
end
end
@@ -379,6 +380,13 @@ class GroupsController < Groups::ApplicationController
def captcha_required?
captcha_enabled? && !params[:parent_id]
end
+
+ def publish_invite_members_for_task_experiment
+ return unless params[:open_modal] == 'invite_members_for_task'
+ return unless current_user&.can?(:admin_group_member, @group)
+
+ experiment(:invite_members_for_task, namespace: @group).publish_to_client
+ end
end
GroupsController.prepend_mod_with('GroupsController')
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 0ad7478584f..e0020c22145 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -122,3 +122,5 @@ class HelpController < ApplicationController
end
end
end
+
+::HelpController.prepend_mod
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index d32755dbd94..cfd86429df0 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -116,7 +116,9 @@ class Import::BitbucketController < Import::BaseController
redirect_to oauth_client.auth_code.authorize_url(redirect_uri: users_import_bitbucket_callback_url)
end
- def bitbucket_unauthorized
+ def bitbucket_unauthorized(exception)
+ log_exception(exception)
+
go_to_bitbucket_for_permissions
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 4242f918ea0..d4b1306cc5e 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -52,7 +52,7 @@ class InvitesController < ApplicationController
end
def current_user_matches_invite?
- current_user.verified_emails.include?(@member.invite_email)
+ current_user.verified_email?(@member.invite_email)
end
def member?
diff --git a/app/controllers/jira_connect/app_descriptor_controller.rb b/app/controllers/jira_connect/app_descriptor_controller.rb
index e96242c7052..16bd73f5ab6 100644
--- a/app/controllers/jira_connect/app_descriptor_controller.rb
+++ b/app/controllers/jira_connect/app_descriptor_controller.rb
@@ -32,7 +32,7 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
apiVersion: 1,
apiMigrations: {
'context-qsh': true,
- 'signed-install': signed_install_active?,
+ 'signed-install': true,
gdpr: true
}
}
diff --git a/app/controllers/jira_connect/application_controller.rb b/app/controllers/jira_connect/application_controller.rb
index ecb23c326fe..352e78d6255 100644
--- a/app/controllers/jira_connect/application_controller.rb
+++ b/app/controllers/jira_connect/application_controller.rb
@@ -74,8 +74,4 @@ class JiraConnect::ApplicationController < ApplicationController
params[:jwt] || request.headers['Authorization']&.split(' ', 2)&.last
end
end
-
- def signed_install_active?
- Feature.enabled?(:jira_connect_asymmetric_jwt)
- end
end
diff --git a/app/controllers/jira_connect/events_controller.rb b/app/controllers/jira_connect/events_controller.rb
index 76ac15f7631..1ea0a92662b 100644
--- a/app/controllers/jira_connect/events_controller.rb
+++ b/app/controllers/jira_connect/events_controller.rb
@@ -4,14 +4,9 @@ class JiraConnect::EventsController < JiraConnect::ApplicationController
# See https://developer.atlassian.com/cloud/jira/software/app-descriptor/#lifecycle
skip_before_action :verify_atlassian_jwt!
- before_action :verify_asymmetric_atlassian_jwt!, if: :signed_install_active?
-
- before_action :verify_atlassian_jwt!, only: :uninstalled, unless: :signed_install_active?
- before_action :verify_qsh_claim!, only: :uninstalled, unless: :signed_install_active?
+ before_action :verify_asymmetric_atlassian_jwt!
def installed
- return head :ok if !signed_install_active? && atlassian_jwt_valid?
-
return head :ok if current_jira_installation
installation = JiraConnectInstallation.new(event_params)
diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb
index 903ad395e44..fcd95c7942c 100644
--- a/app/controllers/jira_connect/subscriptions_controller.rb
+++ b/app/controllers/jira_connect/subscriptions_controller.rb
@@ -7,8 +7,8 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
next if p.directives.blank?
# rubocop: disable Lint/PercentStringArray
- script_src_values = Array.wrap(p.directives['script-src']) | %w('self' https://connect-cdn.atl-paas.net https://unpkg.com/jquery@3.3.1/)
- style_src_values = Array.wrap(p.directives['style-src']) | %w('self' 'unsafe-inline' https://unpkg.com/@atlaskit/)
+ script_src_values = Array.wrap(p.directives['script-src']) | %w('self' https://connect-cdn.atl-paas.net)
+ style_src_values = Array.wrap(p.directives['style-src']) | %w('self' 'unsafe-inline')
# rubocop: enable Lint/PercentStringArray
p.frame_ancestors :self, 'https://*.atlassian.net'
diff --git a/app/controllers/jwks_controller.rb b/app/controllers/jwks_controller.rb
index e7b839f5590..3b0e6ca2eb1 100644
--- a/app/controllers/jwks_controller.rb
+++ b/app/controllers/jwks_controller.rb
@@ -1,13 +1,17 @@
# frozen_string_literal: true
-class JwksController < ActionController::Base # rubocop:disable Rails/ApplicationController
+class JwksController < Doorkeeper::OpenidConnect::DiscoveryController
def index
- render json: { keys: keys }
+ render json: { keys: payload }
+ end
+
+ def keys
+ index
end
private
- def keys
+ def payload
[
# We keep openid_connect_signing_key so that we can seamlessly
# replace it with ci_jwt_signing_key and remove it on the next release.
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 81f188256ba..81084ffe38b 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -24,12 +24,18 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
set_index_vars
end
+ def show
+ @created = get_created_session
+ end
+
def create
@application = Applications::CreateService.new(current_user, application_params).execute(request)
if @application.persisted?
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
+ set_created_session
+
redirect_to oauth_application_url(@application)
else
set_index_vars
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index c764f2d0459..ead5d7c9026 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -1,8 +1,12 @@
# frozen_string_literal: true
class PasswordsController < Devise::PasswordsController
+ include GitlabRecaptcha
+
skip_before_action :require_no_authentication, only: [:edit, :update]
+ prepend_before_action :check_recaptcha, only: :create
+ before_action :load_recaptcha, only: :new
before_action :resource_from_email, only: [:create]
before_action :check_password_authentication_available, only: [:create]
before_action :throttle_reset, only: [:create]
@@ -59,6 +63,12 @@ class PasswordsController < Devise::PasswordsController
alert: _("Password authentication is unavailable.")
end
+ def check_recaptcha
+ return unless resource_params[:email].present?
+
+ super
+ end
+
def throttle_reset
return unless resource && resource.recently_sent_password_reset?
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index e0b5d6be155..e607346b40e 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -66,7 +66,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
render 'create'
else
- @error = _('Invalid pin code')
+ @error = { message: _('Invalid pin code.') }
@qr_code = build_qr_code
if Feature.enabled?(:webauthn)
@@ -147,7 +147,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
def current_password_required?
- !current_user.password_automatically_set?
+ !current_user.password_automatically_set? && current_user.allow_password_authentication_for_web?
end
def build_qr_code
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 69257081cc9..6330a6aa107 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -63,7 +63,7 @@ class ProfilesController < Profiles::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def audit_log
- @events = AuditEvent.where(entity_type: "User", entity_id: current_user.id)
+ @events = AuthenticationEvent.where(user: current_user)
.order("created_at DESC")
.page(params[:page])
diff --git a/app/controllers/projects/alert_management_controller.rb b/app/controllers/projects/alert_management_controller.rb
index 34f9f361e43..ebe867d915d 100644
--- a/app/controllers/projects/alert_management_controller.rb
+++ b/app/controllers/projects/alert_management_controller.rb
@@ -3,10 +3,6 @@
class Projects::AlertManagementController < Projects::ApplicationController
before_action :authorize_read_alert_management_alert!
- before_action(only: [:index]) do
- push_frontend_feature_flag(:managed_alerts_deprecation, @project, default_enabled: :yaml)
- end
-
feature_category :incident_management
def index
diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb
index 95b403faf55..ae8498ce65f 100644
--- a/app/controllers/projects/alerting/notifications_controller.rb
+++ b/app/controllers/projects/alerting/notifications_controller.rb
@@ -18,7 +18,11 @@ module Projects
token = extract_alert_manager_token(request)
result = notify_service.execute(token, integration)
- head result.http_status
+ if result.success?
+ render json: AlertManagement::AlertSerializer.new.represent(result.payload[:alerts]), code: result.http_status
+ else
+ head result.http_status
+ end
end
private
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index 7c419cac1cc..0d5f64c739c 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -3,7 +3,7 @@
class Projects::AutocompleteSourcesController < Projects::ApplicationController
before_action :authorize_read_milestone!, only: :milestones
- feature_category :issue_tracking, [:issues, :labels, :milestones, :commands]
+ feature_category :team_planning, [:issues, :labels, :milestones, :commands]
feature_category :code_review, [:merge_requests]
feature_category :users, [:members]
feature_category :snippets, [:snippets]
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index 0cd59c136e5..8023e51b552 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -6,7 +6,8 @@ class Projects::BadgesController < Projects::ApplicationController
before_action :no_cache_headers, only: [:pipeline, :coverage]
before_action :authorize_read_build!, only: [:pipeline, :coverage]
- feature_category :continuous_integration
+ feature_category :continuous_integration, [:index, :pipeline]
+ feature_category :code_testing, [:coverage]
def pipeline
pipeline_status = Gitlab::Ci::Badge::Pipeline::Status
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 1df7b9ed165..0f87690bba5 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -10,6 +10,7 @@ class Projects::BlameController < Projects::ApplicationController
before_action :authorize_download_code!
feature_category :source_code_management
+ urgency :low, [:show]
def show
@blob = @repository.blob_at(@commit.id, @path)
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 17fd28ee06a..cd50c8cf5b1 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -40,6 +40,7 @@ class Projects::BlobController < Projects::ApplicationController
track_redis_hll_event :create, :update, name: 'g_edit_by_sfe'
feature_category :source_code_management
+ urgency :low, [:create, :show, :edit, :update, :diff]
before_action do
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 834e4baa7dd..7354c2c71ac 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -12,9 +12,13 @@ class Projects::BoardsController < Projects::ApplicationController
push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml)
+ experiment(:prominent_create_board_btn, subject: current_user) do |e|
+ e.use { }
+ e.try { }
+ end.run
end
- feature_category :boards
+ feature_category :team_planning
private
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index b75effc52d1..63ac5f97420 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -14,6 +14,7 @@ class Projects::BranchesController < Projects::ApplicationController
before_action :limit_diverging_commit_counts!, only: [:diverging_commit_counts]
feature_category :source_code_management
+ urgency :low, [:index, :diverging_commit_counts, :create, :destroy]
def index
respond_to do |format|
@@ -105,8 +106,7 @@ class Projects::BranchesController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def destroy
- @branch_name = Addressable::URI.unescape(params[:id])
- result = ::Branches::DeleteService.new(project, current_user).execute(@branch_name)
+ result = ::Branches::DeleteService.new(project, current_user).execute(params[:id])
respond_to do |format|
format.html do
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index 22cd247644d..600516f95a2 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -2,8 +2,8 @@
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
+ before_action :setup_walkthrough_experiment, only: :show
before_action do
- push_frontend_feature_flag(:pipeline_editor_mini_graph, @project, default_enabled: :yaml)
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
end
@@ -17,4 +17,11 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
def check_can_collaborate!
render_404 unless can_collaborate_with_project?(@project)
end
+
+ def setup_walkthrough_experiment
+ experiment(:pipeline_editor_walkthrough, namespace: @project.namespace, sticky_to: current_user) do |e|
+ e.candidate {}
+ e.record!
+ end
+ end
end
diff --git a/app/controllers/projects/cluster_agents_controller.rb b/app/controllers/projects/cluster_agents_controller.rb
index e7fbe93131d..404d3907128 100644
--- a/app/controllers/projects/cluster_agents_controller.rb
+++ b/app/controllers/projects/cluster_agents_controller.rb
@@ -3,6 +3,10 @@
class Projects::ClusterAgentsController < Projects::ApplicationController
before_action :authorize_can_read_cluster_agent!
+ before_action do
+ push_frontend_feature_flag(:cluster_vulnerabilities, project, default_enabled: :yaml)
+ end
+
feature_category :kubernetes_management
def show
@@ -17,3 +21,5 @@ class Projects::ClusterAgentsController < Projects::ApplicationController
access_denied!
end
end
+
+Projects::ClusterAgentsController.prepend_mod_with('Projects::ClusterAgentsController')
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 6748be06ded..62935e133c5 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -24,6 +24,7 @@ class Projects::CommitController < Projects::ApplicationController
COMMIT_DIFFS_PER_PAGE = 20
feature_category :source_code_management
+ urgency :low, [:pipelines, :merge_requests, :show]
def show
apply_diff_view_cookie!
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 9ca917841e9..1ca35903703 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -6,6 +6,8 @@ class Projects::CommitsController < Projects::ApplicationController
include ExtractsPath
include RendersCommits
+ COMMITS_DEFAULT_LIMIT = 40
+
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
around_action :allow_gitaly_ref_name_caching
before_action :require_non_empty_project
@@ -15,6 +17,7 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :set_commits, except: :commits_root
feature_category :source_code_management
+ urgency :low, [:signatures, :show]
def commits_root
redirect_to project_commits_path(@project, @project.default_branch)
@@ -63,7 +66,9 @@ class Projects::CommitsController < Projects::ApplicationController
def set_commits
render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
- @limit = (params[:limit] || 40).to_i
+
+ limit = params[:limit].to_i
+ @limit = limit > 0 ? limit : COMMITS_DEFAULT_LIMIT # limit can only ever be a positive number
@offset = (params[:offset] || 0).to_i
search = params[:search]
author = params[:author]
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 99f62c18593..07f7c1cf7de 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -21,6 +21,7 @@ class Projects::CompareController < Projects::ApplicationController
before_action :validate_refs!
feature_category :source_code_management
+ urgency :low, [:show, :create, :signatures]
# Diffs may be pretty chunky, the less is better in this endpoint.
# Pagination design guides: https://design.gitlab.com/components/pagination/#behavior
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index db5ba51ee01..dc6a9a73d9e 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -6,8 +6,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
include CycleAnalyticsParams
include GracefulTimeoutHandling
include RedisTracking
+ extend ::Gitlab::Utils::Override
before_action :authorize_read_cycle_analytics!
+ before_action :load_value_stream, only: :show
track_redis_hll_event :show, name: 'p_analytics_valuestream'
@@ -19,6 +21,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
def show
@cycle_analytics = Analytics::CycleAnalytics::ProjectLevel.new(project: @project, options: options(cycle_analytics_project_params))
+ @request_params ||= ::Gitlab::Analytics::CycleAnalytics::RequestParams.new(all_cycle_analytics_params)
respond_to do |format|
format.html do
@@ -34,6 +37,15 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
private
+ override :all_cycle_analytics_params
+ def all_cycle_analytics_params
+ super.merge({ project: @project, value_stream: @value_stream })
+ end
+
+ def load_value_stream
+ @value_stream = Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)
+ end
+
def cycle_analytics_json
{
summary: @cycle_analytics.summary,
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index 708b7a6c7ba..9f7d47b95f3 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -9,7 +9,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
before_action :discussion, only: [:resolve, :unresolve]
before_action :authorize_resolve_discussion!, only: [:resolve, :unresolve]
- feature_category :issue_tracking
+ feature_category :team_planning
def resolve
Discussions::ResolveService.new(project, current_user, one_or_more_discussions: discussion).execute
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 23dabd885c8..84ebdcd9364 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -70,11 +70,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def show
- @deployments = environment.deployments.order(id: :desc).page(params[:page])
+ @deployments = environment.deployments.ordered.page(params[:page])
end
- # rubocop: enable CodeReuse/ActiveRecord
def new
@environment = project.environments.new
diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb
index 89e72d98a33..c6bc115e737 100644
--- a/app/controllers/projects/find_file_controller.rb
+++ b/app/controllers/projects/find_file_controller.rb
@@ -11,6 +11,7 @@ class Projects::FindFileController < Projects::ApplicationController
before_action :authorize_download_code!
feature_category :source_code_management
+ urgency :low, [:show, :list]
def show
return render_404 unless @repository.commit(@ref)
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 7135c0d959e..5154f145b46 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -15,6 +15,7 @@ class Projects::ForksController < Projects::ApplicationController
before_action :authorize_fork_namespace!, only: [:create]
feature_category :source_code_management
+ urgency :low, [:index]
before_action do
push_frontend_feature_flag(:fork_project_form, @project, default_enabled: :yaml)
diff --git a/app/controllers/projects/google_cloud_controller.rb b/app/controllers/projects/google_cloud_controller.rb
index d185457aeb3..7257ed1ef6f 100644
--- a/app/controllers/projects/google_cloud_controller.rb
+++ b/app/controllers/projects/google_cloud_controller.rb
@@ -1,16 +1,34 @@
# frozen_string_literal: true
class Projects::GoogleCloudController < Projects::ApplicationController
- before_action :authorize_can_manage_google_cloud_deployments!
+ feature_category :google_cloud
- feature_category :release_orchestration
+ before_action :admin_project_google_cloud?
+ before_action :google_oauth2_enabled?
+ before_action :feature_flag_enabled?
def index
+ @js_data = {
+ serviceAccounts: GoogleCloud::ServiceAccountsService.new(project).find_for_project,
+ createServiceAccountUrl: '#mocked-url-create-service',
+ emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg')
+ }.to_json
end
private
- def authorize_can_manage_google_cloud_deployments!
- access_denied! unless can?(current_user, :manage_project_google_cloud, project)
+ def admin_project_google_cloud?
+ access_denied! unless can?(current_user, :admin_project_google_cloud, project)
+ end
+
+ def google_oauth2_enabled?
+ config = Gitlab::Auth::OAuth::Provider.config_for('google_oauth2')
+ if config.app_id.blank? || config.app_secret.blank?
+ access_denied! 'This GitLab instance not configured for Google Oauth2.'
+ end
+ end
+
+ def feature_flag_enabled?
+ access_denied! unless Feature.enabled?(:incubation_5mp_google_cloud)
end
end
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 7a7961c28bb..d3a05736a47 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -11,7 +11,10 @@ class Projects::GraphsController < Projects::ApplicationController
track_redis_hll_event :charts, name: 'p_analytics_repo'
- feature_category :source_code_management
+ feature_category :source_code_management, [:show, :commits, :languages, :charts]
+ urgency :low, [:show]
+
+ feature_category :continuous_integration, [:ci]
def show
respond_to do |format|
diff --git a/app/controllers/projects/hook_logs_controller.rb b/app/controllers/projects/hook_logs_controller.rb
index 99ebe3335c0..0ca3d71f728 100644
--- a/app/controllers/projects/hook_logs_controller.rb
+++ b/app/controllers/projects/hook_logs_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Projects::HookLogsController < Projects::ApplicationController
- include HooksExecution
+ include ::Integrations::HooksExecution
before_action :authorize_admin_project!
@@ -13,6 +13,7 @@ class Projects::HookLogsController < Projects::ApplicationController
layout 'project_settings'
feature_category :integrations
+ urgency :low, [:retry]
def show
end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index b87bfc58f8b..c79e5a8cc85 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Projects::HooksController < Projects::ApplicationController
- include HooksExecution
+ include ::Integrations::HooksExecution
# Authorize
before_action :authorize_admin_project!
@@ -13,9 +13,10 @@ class Projects::HooksController < Projects::ApplicationController
layout "project_settings"
feature_category :integrations
+ urgency :low, [:test]
def index
- @hooks = @project.hooks
+ @hooks = @project.hooks.load
@hook = ProjectHook.new
end
diff --git a/app/controllers/projects/issue_links_controller.rb b/app/controllers/projects/issue_links_controller.rb
index 35f3e00fae7..e8c3110574f 100644
--- a/app/controllers/projects/issue_links_controller.rb
+++ b/app/controllers/projects/issue_links_controller.rb
@@ -7,7 +7,7 @@ module Projects
before_action :authorize_admin_issue_link!, only: [:create, :destroy]
before_action :authorize_issue_link_association!, only: :destroy
- feature_category :issue_tracking
+ feature_category :team_planning
private
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index fd508d5f127..853e9c7ccdd 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -48,12 +48,11 @@ class Projects::IssuesController < Projects::ApplicationController
end
before_action only: :show do
- real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:real_time_issue_sidebar, @project)
-
- push_to_gon_attributes(:features, :real_time_issue_sidebar, real_time_enabled)
+ push_frontend_feature_flag(:real_time_issue_sidebar, @project, default_enabled: :yaml)
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:paginated_issue_discussions, @project, default_enabled: :yaml)
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
experiment_instance.exclude! unless helpers.can_admin_project_member?(@project)
@@ -71,7 +70,7 @@ class Projects::IssuesController < Projects::ApplicationController
alias_method :designs, :show
- feature_category :issue_tracking, [
+ feature_category :team_planning, [
:index, :calendar, :show, :new, :create, :edit, :update,
:destroy, :move, :reorder, :designs, :toggle_subscription,
:discussions, :bulk_update, :realtime_changes,
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 994be5c2b5c..81b8da9cba3 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -133,9 +133,9 @@ class Projects::JobsController < Projects::ApplicationController
end
def raw
- if trace_artifact_file
+ if @build.trace.archived_trace_exist?
workhorse_set_content_type!
- send_upload(trace_artifact_file,
+ send_upload(@build.job_artifacts_trace.file,
send_params: raw_send_params,
redirect_params: raw_redirect_params)
else
@@ -219,10 +219,6 @@ class Projects::JobsController < Projects::ApplicationController
params.permit(job_variables_attributes: %i[key secret_value])
end
- def trace_artifact_file
- @trace_artifact_file ||= @build.job_artifacts_trace&.file
- end
-
def find_job_as_build
@build = project.builds.find(params[:id])
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 6bf3885fb7a..814081194d6 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -14,7 +14,7 @@ class Projects::LabelsController < Projects::ApplicationController
respond_to :js, :html
- feature_category :issue_tracking
+ feature_category :team_planning
def index
respond_to do |format|
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 46df514abcb..6c5a8aa0610 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -41,6 +41,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:diffs_virtual_scrolling, project, default_enabled: :yaml)
push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
push_frontend_feature_flag(:mr_changes_fluid_layout, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:mr_attention_requests, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml)
# Usage data feature flags
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
@@ -140,8 +142,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
cache_context = [
params[:serializer],
current_user&.cache_key,
- @merge_request.assignees.map(&:cache_key),
- @merge_request.reviewers.map(&:cache_key)
+ @merge_request.merge_request_assignees.map(&:cache_key),
+ @merge_request.merge_request_reviewers.map(&:cache_key)
]
render_cached(@merge_request,
diff --git a/app/controllers/projects/metrics_dashboard_controller.rb b/app/controllers/projects/metrics_dashboard_controller.rb
index 89c99a5fd5a..3f10749602e 100644
--- a/app/controllers/projects/metrics_dashboard_controller.rb
+++ b/app/controllers/projects/metrics_dashboard_controller.rb
@@ -12,7 +12,6 @@ module Projects
before_action do
push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate)
- push_frontend_feature_flag(:managed_alerts_deprecation, @project, default_enabled: :yaml)
end
feature_category :metrics
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 630e7ccd43f..5dc9718d7a4 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -18,7 +18,7 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to :html
- feature_category :issue_tracking
+ feature_category :team_planning
def index
@sort = params[:sort] || 'due_date_asc'
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index f3a7bc7913e..84ac9fb01fd 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -11,6 +11,7 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :assign_commit
feature_category :source_code_management
+ urgency :low, [:show]
def show
@url = project_network_path(@project, @ref, @options.merge(format: :json))
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index e7e6aed8ec8..e8057308386 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -11,7 +11,7 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_create_note!, only: [:create]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
- feature_category :issue_tracking
+ feature_category :team_planning
def delete_attachment
note.remove_attachment!
@@ -55,6 +55,14 @@ class Projects::NotesController < Projects::ApplicationController
end
end
+ def outdated_line_change
+ diff_lines = Rails.cache.fetch(['note', note.id, 'oudated_line_change'], expires_in: 7.days) do
+ ::MergeRequests::OutdatedDiscussionDiffLinesService.new(project: @project, note: note).execute.to_json
+ end
+
+ render json: diff_lines
+ end
+
private
def render_json_with_notes_serializer
diff --git a/app/controllers/projects/packages/packages_controller.rb b/app/controllers/projects/packages/packages_controller.rb
index dd7c2ad3cbd..5de71466c10 100644
--- a/app/controllers/projects/packages/packages_controller.rb
+++ b/app/controllers/projects/packages/packages_controller.rb
@@ -7,10 +7,6 @@ module Projects
feature_category :package_registry
- before_action do
- push_frontend_feature_flag(:package_list_apollo, default_enabled: :yaml)
- end
-
def show
@package = project.packages.find(params[:id])
end
diff --git a/app/controllers/projects/prometheus/alerts_controller.rb b/app/controllers/projects/prometheus/alerts_controller.rb
index 19c908026cf..312919831d4 100644
--- a/app/controllers/projects/prometheus/alerts_controller.rb
+++ b/app/controllers/projects/prometheus/alerts_controller.rb
@@ -30,7 +30,11 @@ module Projects
token = extract_alert_manager_token(request)
result = notify_service.execute(token)
- head result.http_status
+ if result.success?
+ render json: AlertManagement::AlertSerializer.new.represent(result.payload[:alerts]), code: result.http_status
+ else
+ head result.http_status
+ end
end
def create
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 4d23c853334..73eb6bb2bf2 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -12,6 +12,7 @@ class Projects::RefsController < Projects::ApplicationController
before_action :authorize_download_code!
feature_category :source_code_management
+ urgency :low, [:switch, :logs_tree]
def switch
respond_to do |format|
diff --git a/app/controllers/projects/releases/evidences_controller.rb b/app/controllers/projects/releases/evidences_controller.rb
index 1e2dbf8047c..41e2ce81eb8 100644
--- a/app/controllers/projects/releases/evidences_controller.rb
+++ b/app/controllers/projects/releases/evidences_controller.rb
@@ -20,7 +20,6 @@ module Projects
private
def authorize_read_release_evidence!
- access_denied! unless Feature.enabled?(:release_evidence, project, default_enabled: true)
access_denied! unless can?(current_user, :read_release_evidence, evidence)
end
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index be2abc5cddf..7fba6cc5bf4 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -5,9 +5,6 @@ class Projects::ReleasesController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:index]
before_action :release, only: %i[edit show update downloads]
before_action :authorize_read_release!
- # We have to check `download_code` permission because detail URL path
- # contains git-tag name.
- before_action :authorize_download_code!, except: [:index]
before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_create_release!, only: :new
before_action only: :index do
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index c42d382c4bb..9464826701d 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -18,6 +18,7 @@ class Projects::ServicesController < Projects::ApplicationController
layout "project_settings"
feature_category :integrations
+ urgency :low, [:test]
def edit
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 3033dac8246..4fe37352995 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -104,8 +104,7 @@ module Projects
CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
# rubocop:enable CodeReuse/Worker
- pipelines_link_start = '<a href="%{url}">'.html_safe % { url: project_pipelines_path(@project) }
- flash[:toast] = _("A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details") % { pipelines_link_start: pipelines_link_start, pipelines_link_end: "</a>".html_safe }
+ flash[:toast] = _("A new Auto DevOps pipeline has been created, go to the Pipelines page for details")
end
def define_variables
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 728231dbdbd..cc419bab687 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -12,6 +12,7 @@ module Projects
feature_category :source_code_management, [:show, :cleanup]
feature_category :continuous_delivery, [:create_deploy_token]
+ urgency :low, [:show]
def show
render_show
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 02d36c3353d..de0faaca9c0 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -10,23 +10,29 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_download_code!
before_action :authorize_admin_tag!, only: [:new, :create, :destroy]
- feature_category :source_code_management, [:index, :show, :new, :destroy]
- feature_category :release_evidence, [:create]
+ feature_category :source_code_management
+ urgency :low, [:new, :show, :index]
# rubocop: disable CodeReuse/ActiveRecord
def index
- params[:sort] = params[:sort].presence || sort_value_recently_updated
+ begin
+ params[:sort] = params[:sort].presence || sort_value_recently_updated
- @sort = params[:sort]
+ @sort = params[:sort]
- @tags, @tags_loading_error = TagsFinder.new(@repository, params).execute
+ @tags = TagsFinder.new(@repository, params).execute
- @tags = Kaminari.paginate_array(@tags).page(params[:page])
- tag_names = @tags.map(&:name)
- @tags_pipelines = @project.ci_pipelines.latest_successful_for_refs(tag_names)
+ @tags = Kaminari.paginate_array(@tags).page(params[:page])
+ tag_names = @tags.map(&:name)
+ @tags_pipelines = @project.ci_pipelines.latest_successful_for_refs(tag_names)
- @releases = project.releases.where(tag: tag_names)
- @tag_pipeline_statuses = Ci::CommitStatusesFinder.new(@project, @repository, current_user, @tags).execute
+ @releases = project.releases.where(tag: tag_names)
+ @tag_pipeline_statuses = Ci::CommitStatusesFinder.new(@project, @repository, current_user, @tags).execute
+
+ rescue Gitlab::Git::CommandError => e
+ @tags = []
+ @tags_loading_error = e
+ end
respond_to do |format|
status = @tags_loading_error ? :service_unavailable : :ok
diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb
index 4bad6dc1b3d..6d06b05c1e9 100644
--- a/app/controllers/projects/templates_controller.rb
+++ b/app/controllers/projects/templates_controller.rb
@@ -6,6 +6,7 @@ class Projects::TemplatesController < Projects::ApplicationController
before_action :get_template_class
feature_category :source_code_management
+ urgency :low, [:names]
def index
templates = @template_type.template_subsets(project)
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index 6ba89ab34f8..dafdeb4c9ef 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -6,7 +6,7 @@ class Projects::TodosController < Projects::ApplicationController
before_action :authenticate_user!, only: [:create]
- feature_category :issue_tracking
+ feature_category :team_planning
private
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index a76d45411dd..f8f2c1f0836 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -17,11 +17,12 @@ class Projects::TreeController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml)
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
end
feature_category :source_code_management
+ urgency :low, [:show]
def show
return render_404 unless @commit
diff --git a/app/controllers/projects/usage_quotas_controller.rb b/app/controllers/projects/usage_quotas_controller.rb
index 103e1cc596a..b319e427eaa 100644
--- a/app/controllers/projects/usage_quotas_controller.rb
+++ b/app/controllers/projects/usage_quotas_controller.rb
@@ -2,7 +2,6 @@
class Projects::UsageQuotasController < Projects::ApplicationController
before_action :authorize_admin_project!
- before_action :verify_usage_quotas_enabled!
layout "project_settings"
@@ -20,10 +19,4 @@ class Projects::UsageQuotasController < Projects::ApplicationController
wiki_help_page_path: help_page_path('administration/wikis/index.md', anchor: 'reduce-wiki-repository-size')
}
end
-
- private
-
- def verify_usage_quotas_enabled!
- render_404 unless Feature.enabled?(:project_storage_ui, project&.group, default_enabled: :yaml)
- end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 9ee8847004e..02dfaf4c193 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -3,11 +3,9 @@
class Projects::WikisController < Projects::ApplicationController
include WikiActions
- alias_method :container, :project
+ urgency :low
- before_action do
- push_frontend_feature_flag(:content_editor_block_tables, @project, default_enabled: :yaml)
- end
+ alias_method :container, :project
feature_category :wiki
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 0760f97d7c1..5b17b75a963 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -37,7 +37,6 @@ class ProjectsController < Projects::ApplicationController
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_text_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml)
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
end
@@ -49,9 +48,10 @@ class ProjectsController < Projects::ApplicationController
]
feature_category :source_code_management, [:remove_fork, :housekeeping, :refs]
- feature_category :issue_tracking, [:preview_markdown, :new_issuable_address]
+ feature_category :team_planning, [:preview_markdown, :new_issuable_address]
feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export]
feature_category :code_review, [:unfoldered_environment_names]
+ urgency :low, [:refs]
def index
redirect_to(current_user ? root_path : explore_root_path)
@@ -293,7 +293,11 @@ class ProjectsController < Projects::ApplicationController
end
if find_tags && @repository.tag_count.nonzero?
- tags, _ = TagsFinder.new(@repository, params).execute
+ tags = begin
+ TagsFinder.new(@repository, params).execute
+ rescue Gitlab::Git::CommandError
+ []
+ end
options['Tags'] = tags.take(100).map(&:name)
end
@@ -336,11 +340,6 @@ class ProjectsController < Projects::ApplicationController
if can?(current_user, :download_code, @project)
return render 'projects/no_repo' unless @project.repository_exists?
- if @project.can_current_user_push_to_default_branch?
- property = @project.empty_repo? ? 'empty' : 'nonempty'
- experiment(:empty_repo_upload, project: @project).track(:view_project_show, property: property)
- end
-
render 'projects/empty' if @project.empty_repo?
else
if can?(current_user, :read_wiki, @project)
@@ -452,6 +451,7 @@ class ProjectsController < Projects::ApplicationController
:suggestion_commit_message,
:packages_enabled,
:service_desk_enabled,
+ :merge_commit_template,
project_setting_attributes: project_setting_attributes
] + [project_feature_attributes: project_feature_attributes]
end
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
index 416bbf43464..39d3125a4a3 100644
--- a/app/controllers/registrations/welcome_controller.rb
+++ b/app/controllers/registrations/welcome_controller.rb
@@ -16,7 +16,9 @@ module Registrations
result = ::Users::SignupService.new(current_user, update_params).execute
if result[:status] == :success
- return redirect_to experiment(:combined_registration, user: current_user).redirect_path(trial_params) if show_signup_onboarding?
+ return redirect_to issues_dashboard_path(assignee_username: current_user.username) if show_tasks_to_be_done?
+
+ return redirect_to update_success_path if show_signup_onboarding?
members = current_user.members
@@ -64,12 +66,23 @@ module Registrations
members.last.source.activity_path
end
+ # overridden in EE
def show_signup_onboarding?
false
end
+ def show_tasks_to_be_done?
+ return unless experiment(:invite_members_for_task).enabled?
+
+ MemberTask.for_members(current_user.members).exists?
+ end
+
+ # overridden in EE
def trial_params
- nil
+ end
+
+ # overridden in EE
+ def update_success_path
end
end
end
diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb
index a7719516cb6..30cafb6747e 100644
--- a/app/controllers/repositories/lfs_api_controller.rb
+++ b/app/controllers/repositories/lfs_api_controller.rb
@@ -13,6 +13,7 @@ module Repositories
# added here as a part of the refactor, will be removed
# https://gitlab.com/gitlab-org/gitlab/-/issues/328692
delegate :deploy_token, :user, to: :authentication_result, allow_nil: true
+ urgency :medium, [:batch]
def batch
unless objects.present?
diff --git a/app/controllers/repositories/lfs_storage_controller.rb b/app/controllers/repositories/lfs_storage_controller.rb
index 6ec63a0f939..252b604dcb0 100644
--- a/app/controllers/repositories/lfs_storage_controller.rb
+++ b/app/controllers/repositories/lfs_storage_controller.rb
@@ -11,6 +11,8 @@ module Repositories
# added here as a part of the refactor, will be removed
# https://gitlab.com/gitlab-org/gitlab/-/issues/328692
delegate :deploy_token, :user, to: :authentication_result, allow_nil: true
+ urgency :medium, [:download, :upload_authorize]
+ urgency :low, [:upload_finalize]
def download
lfs_object = LfsObject.find_by_oid(oid)
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 0a18559fc81..0e285dae089 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -46,6 +46,7 @@ class SearchController < ApplicationController
@search_results = @search_service.search_results
@search_objects = @search_service.search_objects
@search_highlight = @search_service.search_highlight
+ @aggregations = @search_service.search_aggregations
increment_search_counters
end
@@ -151,7 +152,7 @@ class SearchController < ApplicationController
def block_anonymous_global_searches
return if params[:project_id].present? || params[:group_id].present?
return if current_user
- return unless ::Feature.enabled?(:block_anonymous_global_searches)
+ return unless ::Feature.enabled?(:block_anonymous_global_searches, type: :ops)
store_location_for(:user, request.fullpath)
diff --git a/app/experiments/change_continuous_onboarding_link_urls_experiment.rb b/app/experiments/change_continuous_onboarding_link_urls_experiment.rb
new file mode 100644
index 00000000000..680cb8eadd8
--- /dev/null
+++ b/app/experiments/change_continuous_onboarding_link_urls_experiment.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ChangeContinuousOnboardingLinkUrlsExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
+ attr_writer :namespace
+
+ def track(action, **event_args)
+ super(action, **event_args.merge(namespace: @namespace))
+ end
+end
diff --git a/app/experiments/templates/new_project_readme_content/readme_advanced.md.tt b/app/experiments/templates/new_project_readme_content/readme_advanced.md.tt
index bbe8271386f..7592c7c6ab7 100644
--- a/app/experiments/templates/new_project_readme_content/readme_advanced.md.tt
+++ b/app/experiments/templates/new_project_readme_content/readme_advanced.md.tt
@@ -23,13 +23,14 @@ git push -uf origin <%= @project.default_branch_or_main %>
## Integrate with your tools
-- [ ] [Set up project integrations](<%= redirect("https://docs.gitlab.com/ee/user/project/integrations/") %>)
+- [ ] [Set up project integrations](<%= redirect(project_settings_integrations_url(@project)) %>)
## Collaborate with your team
- [ ] [Invite team members and collaborators](<%= redirect("https://docs.gitlab.com/ee/user/project/members/") %>)
- [ ] [Create a new merge request](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html") %>)
- [ ] [Automatically close issues from merge requests](<%= redirect("https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically") %>)
+- [ ] [Enable merge request approvals](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/approvals/") %>)
- [ ] [Automatically merge when pipeline succeeds](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html") %>)
## Test and Deploy
@@ -40,6 +41,7 @@ Use the built-in continuous integration in GitLab.
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](<%= redirect("https://docs.gitlab.com/ee/user/application_security/sast/") %>)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](<%= redirect("https://docs.gitlab.com/ee/topics/autodevops/requirements.html") %>)
- [ ] [Use pull-based deployments for improved Kubernetes management](<%= redirect("https://docs.gitlab.com/ee/user/clusters/agent/") %>)
+- [ ] [Set up protected environments](<%= redirect("https://docs.gitlab.com/ee/ci/environments/protected_environments.html") %>)
***
diff --git a/app/finders/autocomplete/routes_finder.rb b/app/finders/autocomplete/routes_finder.rb
index b3f2693b273..858a4b69376 100644
--- a/app/finders/autocomplete/routes_finder.rb
+++ b/app/finders/autocomplete/routes_finder.rb
@@ -30,7 +30,7 @@ module Autocomplete
class NamespacesOnly < self
def routables
- return Namespace.all if current_user.admin?
+ return Namespace.without_project_namespaces if current_user.admin?
current_user.namespaces
end
diff --git a/app/finders/award_emojis_finder.rb b/app/finders/award_emojis_finder.rb
index 9ff64637128..709d3f3e593 100644
--- a/app/finders/award_emojis_finder.rb
+++ b/app/finders/award_emojis_finder.rb
@@ -40,7 +40,7 @@ class AwardEmojisFinder
def validate_name_param
return unless params[:name]
- raise ArgumentError, 'Invalid name param' unless params[:name].to_s.in?(Gitlab::Emoji.emojis_names)
+ raise ArgumentError, 'Invalid name param' unless TanukiEmoji.find_by_alpha_code(params[:name].to_s)
end
def validate_awarded_by_param
diff --git a/app/finders/ci/pipelines_finder.rb b/app/finders/ci/pipelines_finder.rb
index 39355853d88..712d5f8c6fb 100644
--- a/app/finders/ci/pipelines_finder.rb
+++ b/app/finders/ci/pipelines_finder.rb
@@ -5,6 +5,13 @@ module Ci
attr_reader :project, :pipelines, :params, :current_user
ALLOWED_INDEXED_COLUMNS = %w[id status ref updated_at user_id].freeze
+ ALLOWED_SCOPES = {
+ RUNNING: 'running',
+ PENDING: 'pending',
+ FINISHED: 'finished',
+ BRANCHES: 'branches',
+ TAGS: 'tags'
+ }.freeze
def initialize(project, current_user, params = {})
@project = project
@@ -65,15 +72,15 @@ module Ci
def by_scope(items)
case params[:scope]
- when 'running'
+ when ALLOWED_SCOPES[:RUNNING]
items.running
- when 'pending'
+ when ALLOWED_SCOPES[:PENDING]
items.pending
- when 'finished'
+ when ALLOWED_SCOPES[:FINISHED]
items.finished
- when 'branches'
+ when ALLOWED_SCOPES[:BRANCHES]
from_ids(ids_for_ref(branches))
- when 'tags'
+ when ALLOWED_SCOPES[:TAGS]
from_ids(ids_for_ref(tags))
else
items
diff --git a/app/finders/ci/pipelines_for_merge_request_finder.rb b/app/finders/ci/pipelines_for_merge_request_finder.rb
index 9476c30f525..b623a94541b 100644
--- a/app/finders/ci/pipelines_for_merge_request_finder.rb
+++ b/app/finders/ci/pipelines_for_merge_request_finder.rb
@@ -31,65 +31,27 @@ module Ci
# Fetch all pipelines without permission check.
def all
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336891') do
- strong_memoize(:all_pipelines) do
- next Ci::Pipeline.none unless source_project
-
- pipelines =
- if merge_request.persisted?
- all_pipelines_for_merge_request
- else
- triggered_for_branch.for_sha(commit_shas)
- end
-
- sort(pipelines)
- end
- end
- end
-
- private
+ strong_memoize(:all_pipelines) do
+ next Ci::Pipeline.none unless source_project
- # rubocop: disable CodeReuse/ActiveRecord
- def pipelines_using_cte
- sha_relation = merge_request.all_commits.select(:sha).distinct
-
- cte = Gitlab::SQL::CTE.new(:shas, sha_relation)
-
- pipelines_for_merge_requests = triggered_by_merge_request
- pipelines_for_branch = filter_by_sha(triggered_for_branch, cte)
-
- Ci::Pipeline.with(cte.to_arel) # rubocop: disable CodeReuse/ActiveRecord
- .from_union([pipelines_for_merge_requests, pipelines_for_branch])
- end
- # rubocop: enable CodeReuse/ActiveRecord
+ pipelines =
+ if merge_request.persisted?
+ all_pipelines_for_merge_request
+ else
+ triggered_for_branch.for_sha(commit_shas)
+ end
- def filter_by_sha(pipelines, cte)
- hex = Arel::Nodes::SqlLiteral.new("'hex'")
- string_sha = Arel::Nodes::NamedFunction.new('encode', [cte.table[:sha], hex])
- join_condition = string_sha.eq(Ci::Pipeline.arel_table[:sha])
-
- filter_by(pipelines, cte, join_condition)
+ sort(pipelines)
+ end
end
- def filter_by(pipelines, cte, join_condition)
- shas_table =
- Ci::Pipeline.arel_table
- .join(cte.table, Arel::Nodes::InnerJoin)
- .on(join_condition)
- .join_sources
-
- pipelines.joins(shas_table) # rubocop: disable CodeReuse/ActiveRecord
- end
+ private
def all_pipelines_for_merge_request
- if Feature.enabled?(:decomposed_ci_query_in_pipelines_for_merge_request_finder, target_project, default_enabled: :yaml)
- pipelines_for_merge_request = triggered_by_merge_request
- pipelines_for_branch = triggered_for_branch.for_sha(recent_diff_head_shas(COMMITS_LIMIT))
+ pipelines_for_merge_request = triggered_by_merge_request
+ pipelines_for_branch = triggered_for_branch.for_sha(recent_diff_head_shas(COMMITS_LIMIT))
- Ci::Pipeline.from_union([pipelines_for_merge_request, pipelines_for_branch])
- else
- pipelines_using_cte
- end
+ Ci::Pipeline.from_union([pipelines_for_merge_request, pipelines_for_branch])
end
# NOTE: this method returns only parent merge request pipelines.
diff --git a/app/finders/clusters/agent_authorizations_finder.rb b/app/finders/clusters/agent_authorizations_finder.rb
new file mode 100644
index 00000000000..373cf7fe8b9
--- /dev/null
+++ b/app/finders/clusters/agent_authorizations_finder.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Clusters
+ class AgentAuthorizationsFinder
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ # closest, most-specific authorization for a given agent wins
+ (project_authorizations + implicit_authorizations + group_authorizations)
+ .uniq(&:agent_id)
+ end
+
+ private
+
+ attr_reader :project
+
+ def implicit_authorizations
+ project.cluster_agents.map do |agent|
+ Clusters::Agents::ImplicitAuthorization.new(agent: agent)
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def project_authorizations
+ ancestor_ids = project.group ? project.ancestors.select(:id) : project.namespace_id
+
+ Clusters::Agents::ProjectAuthorization
+ .where(project_id: project.id)
+ .joins(agent: :project)
+ .preload(agent: :project)
+ .where(cluster_agents: { projects: { namespace_id: ancestor_ids } })
+ .with_available_ci_access_fields(project)
+ .to_a
+ end
+
+ def group_authorizations
+ return [] unless project.group
+
+ authorizations = Clusters::Agents::GroupAuthorization.arel_table
+
+ ordered_ancestors_cte = Gitlab::SQL::CTE.new(
+ :ordered_ancestors,
+ project.group.self_and_ancestors(hierarchy_order: :asc).reselect(:id)
+ )
+
+ cte_join_sources = authorizations.join(ordered_ancestors_cte.table).on(
+ authorizations[:group_id].eq(ordered_ancestors_cte.table[:id])
+ ).join_sources
+
+ Clusters::Agents::GroupAuthorization
+ .with(ordered_ancestors_cte.to_arel)
+ .joins(cte_join_sources)
+ .joins(agent: :project)
+ .where('projects.namespace_id IN (SELECT id FROM ordered_ancestors)')
+ .with_available_ci_access_fields(project)
+ .order(Arel.sql('agent_id, array_position(ARRAY(SELECT id FROM ordered_ancestors)::bigint[], agent_group_authorizations.group_id)'))
+ .select('DISTINCT ON (agent_id) agent_group_authorizations.*')
+ .preload(agent: :project)
+ .to_a
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/app/finders/environments/environments_by_deployments_finder.rb b/app/finders/environments/environments_by_deployments_finder.rb
index e0ecc98b1c0..a41cfcb37e4 100644
--- a/app/finders/environments/environments_by_deployments_finder.rb
+++ b/app/finders/environments/environments_by_deployments_finder.rb
@@ -23,12 +23,18 @@ module Environments
deployments.none
end
- environment_ids = deployments
- .group(:environment_id)
- .select(:environment_id)
+ environments =
+ if Feature.enabled?(:environments_by_deployments_finder_exists_optimization, default_enabled: :yaml)
+ project.environments.available
+ .where('EXISTS (?)', deployments.where('environment_id = environments.id'))
+ else
+ environment_ids = deployments
+ .group(:environment_id)
+ .select(:environment_id)
- environments = project.environments.available
- .where(id: environment_ids)
+ project.environments.available
+ .where(id: environment_ids)
+ end
if params[:find_latest]
find_one(environments.order_by_last_deployed_at_desc)
diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb
index 4c619f3d7ea..4ed447a90ce 100644
--- a/app/finders/events_finder.rb
+++ b/app/finders/events_finder.rb
@@ -44,7 +44,6 @@ class EventsFinder
events = by_created_at_after(events)
events = sort(events)
- events = events.with_associations if params[:with_associations]
paginated_filtered_by_user_visibility(events)
end
@@ -113,10 +112,12 @@ class EventsFinder
end
def paginated_filtered_by_user_visibility(events)
+ events_count = events.count
+ events = events.with_associations if params[:with_associations]
limited_events = events.page(page).per(per_page)
visible_events = limited_events.select { |event| event.visible_to_user?(current_user) }
- Kaminari.paginate_array(visible_events, total_count: events.count)
+ Kaminari.paginate_array(visible_events, total_count: events_count)
end
def per_page
diff --git a/app/finders/issuables/label_filter.rb b/app/finders/issuables/label_filter.rb
index f4712fa6879..9a6ca107b19 100644
--- a/app/finders/issuables/label_filter.rb
+++ b/app/finders/issuables/label_filter.rb
@@ -82,6 +82,8 @@ module Issuables
if root_namespace
label_ids = find_label_ids(label_names).flatten(1)
+ return issuables if label_ids.empty?
+
issuables.where(label_link_query(target_model, label_ids: label_ids).arel.exists.not)
else
issuables.where(label_link_query(target_model, label_names: label_names).arel.exists.not)
@@ -133,11 +135,7 @@ module Issuables
# rubocop: disable CodeReuse/ActiveRecord
def label_link_query(target_model, label_ids: nil, label_names: nil)
- relation = LabelLink
- .where(target_type: target_model.name)
- .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id']))
-
- relation = relation.where(label_id: label_ids) if label_ids
+ relation = LabelLink.by_target_for_exists_query(target_model.name, target_model.arel_table['id'], label_ids)
relation = relation.joins(:label).where(labels: { name: label_names }) if label_names
relation
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index 0faafa6df9c..e68a0c8fca9 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -70,16 +70,11 @@ class MembersFinder
end
def project_invited_groups
- invited_groups_and_ancestors = if ::Feature.enabled?(:linear_members_finder_ancestor_scopes, current_user, default_enabled: :yaml)
- project.invited_groups
- .self_and_ancestors
- else
- Gitlab::ObjectHierarchy
- .new(project.invited_groups)
- .base_and_ancestors
- end
-
- invited_groups_ids_including_ancestors = invited_groups_and_ancestors.public_or_visible_to_user(current_user).select(:id)
+ invited_groups_ids_including_ancestors = project
+ .invited_groups
+ .self_and_ancestors
+ .public_or_visible_to_user(current_user)
+ .select(:id)
GroupMember.with_source_id(invited_groups_ids_including_ancestors).non_minimal_access
end
diff --git a/app/finders/packages/group_packages_finder.rb b/app/finders/packages/group_packages_finder.rb
index 3ac5f00d518..2a62dd5c0e5 100644
--- a/app/finders/packages/group_packages_finder.rb
+++ b/app/finders/packages/group_packages_finder.rb
@@ -22,7 +22,7 @@ module Packages
def packages_for_group_projects(installable_only: false)
packages = ::Packages::Package
- .including_build_info
+ .preload_pipelines
.including_project_route
.including_tags
.for_projects(group_projects_visible_to_current_user.select(:id))
diff --git a/app/finders/packages/package_finder.rb b/app/finders/packages/package_finder.rb
index ee96896e350..e482a0503f0 100644
--- a/app/finders/packages/package_finder.rb
+++ b/app/finders/packages/package_finder.rb
@@ -9,7 +9,7 @@ module Packages
def execute
@project
.packages
- .including_build_info
+ .preload_pipelines
.including_project_route
.including_tags
.displayable
diff --git a/app/finders/packages/packages_finder.rb b/app/finders/packages/packages_finder.rb
index 552468ecfd1..3bc348c8dc8 100644
--- a/app/finders/packages/packages_finder.rb
+++ b/app/finders/packages/packages_finder.rb
@@ -14,9 +14,10 @@ module Packages
def execute
packages = project.packages
- .including_build_info
+ .preload_pipelines
.including_project_route
.including_tags
+
packages = filter_with_version(packages)
packages = filter_by_package_type(packages)
packages = filter_by_package_name(packages)
diff --git a/app/finders/security/security_jobs_finder.rb b/app/finders/security/security_jobs_finder.rb
index 008d4e29b13..5754492cfa7 100644
--- a/app/finders/security/security_jobs_finder.rb
+++ b/app/finders/security/security_jobs_finder.rb
@@ -13,7 +13,7 @@
module Security
class SecurityJobsFinder < JobsFinder
def self.allowed_job_types
- [:sast, :dast, :dependency_scanning, :container_scanning, :secret_detection, :coverage_fuzzing, :api_fuzzing, :cluster_image_scanning]
+ [:sast, :sast_iac, :dast, :dependency_scanning, :container_scanning, :secret_detection, :coverage_fuzzing, :api_fuzzing, :cluster_image_scanning]
end
end
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 81643826782..b1e12721712 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -42,12 +42,11 @@ class SnippetsFinder < UnionFinder
include FinderMethods
include Gitlab::Utils::StrongMemoize
- attr_accessor :current_user, :params
- delegate :explore, :only_personal, :only_project, :scope, :sort, to: :params
+ attr_reader :current_user, :params
def initialize(current_user = nil, params = {})
@current_user = current_user
- @params = OpenStruct.new(params)
+ @params = params
if project && author
raise(
@@ -77,9 +76,9 @@ class SnippetsFinder < UnionFinder
private
def init_collection
- if explore
+ if explore?
snippets_for_explore
- elsif only_personal
+ elsif only_personal?
personal_snippets
elsif project
snippets_for_a_single_project
@@ -110,7 +109,7 @@ class SnippetsFinder < UnionFinder
# over the resulting SQL query.
def snippets_for_personal_and_multiple_projects
queries = []
- queries << personal_snippets unless only_project
+ queries << personal_snippets unless only_project?
if Ability.allowed?(current_user, :read_cross_project)
queries << snippets_of_visible_projects
@@ -171,7 +170,7 @@ class SnippetsFinder < UnionFinder
end
def visibility_from_scope
- case scope.to_s
+ case params[:scope].to_s
when 'are_private'
Snippet::PRIVATE
when 'are_internal'
@@ -206,7 +205,19 @@ class SnippetsFinder < UnionFinder
end
def sort_param
- sort.presence || 'id_desc'
+ params[:sort].presence || 'id_desc'
+ end
+
+ def explore?
+ params[:explore].present?
+ end
+
+ def only_personal?
+ params[:only_personal].present?
+ end
+
+ def only_project?
+ params[:only_project].present?
end
def prepared_union(queries)
diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb
index 0ccbbdc1b87..6bc5419e704 100644
--- a/app/finders/tags_finder.rb
+++ b/app/finders/tags_finder.rb
@@ -5,11 +5,36 @@ class TagsFinder < GitRefsFinder
super(repository, params)
end
- def execute
- tags = repository.tags_sorted_by(sort)
+ def execute(gitaly_pagination: false)
+ tags = if gitaly_pagination
+ repository.tags_sorted_by(sort, pagination_params)
+ else
+ repository.tags_sorted_by(sort)
+ end
- [by_search(tags), nil]
- rescue Gitlab::Git::CommandError => e
- [[], e]
+ by_search(tags)
+
+ rescue ArgumentError => e
+ raise Gitlab::Git::InvalidPageToken, "Invalid page token: #{page_token}" if e.message.include?('page token')
+
+ raise
+ end
+
+ def total
+ repository.tag_count
+ end
+
+ private
+
+ def per_page
+ params[:per_page].presence
+ end
+
+ def page_token
+ "#{Gitlab::Git::TAG_REF_PREFIX}#{@params[:page_token]}" if params[:page_token]
+ end
+
+ def pagination_params
+ { limit: per_page, page_token: page_token }
end
end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 38ba1611c48..e15a185a743 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -10,11 +10,17 @@ class GitlabSchema < GraphQL::Schema
DEFAULT_MAX_DEPTH = 15
AUTHENTICATED_MAX_DEPTH = 20
+ # Tracers (order is important)
+ use Gitlab::Graphql::Tracers::ApplicationContextTracer
+ use Gitlab::Graphql::Tracers::MetricsTracer
+ use Gitlab::Graphql::Tracers::LoggerTracer
+ use Gitlab::Graphql::GenericTracing # Old tracer which will be removed eventually
+ use Gitlab::Graphql::Tracers::TimerTracer
+
use GraphQL::Subscriptions::ActionCableSubscriptions
use GraphQL::Pagination::Connections
use BatchLoader::GraphQL
use Gitlab::Graphql::Pagination::Connections
- use Gitlab::Graphql::GenericTracing
use Gitlab::Graphql::Timeout, max_seconds: Gitlab.config.gitlab.graphql_timeout
query_analyzer Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer.new
diff --git a/app/graphql/mutations/customer_relations/contacts/create.rb b/app/graphql/mutations/customer_relations/contacts/create.rb
index 77b4864468b..3495f30f227 100644
--- a/app/graphql/mutations/customer_relations/contacts/create.rb
+++ b/app/graphql/mutations/customer_relations/contacts/create.rb
@@ -42,13 +42,11 @@ module Mutations
required: false,
description: 'Description of or notes for the contact.'
- authorize :admin_contact
+ authorize :admin_crm_contact
def resolve(args)
group = authorized_find!(id: args[:group_id])
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, group, default_enabled: :yaml)
-
set_organization!(args)
result = ::CustomerRelations::Contacts::CreateService.new(group: group, current_user: current_user, params: args).execute
{ contact: result.payload, errors: result.errors }
diff --git a/app/graphql/mutations/customer_relations/contacts/update.rb b/app/graphql/mutations/customer_relations/contacts/update.rb
index e9e7c9b6abd..e2f671058f0 100644
--- a/app/graphql/mutations/customer_relations/contacts/update.rb
+++ b/app/graphql/mutations/customer_relations/contacts/update.rb
@@ -8,7 +8,7 @@ module Mutations
graphql_name 'CustomerRelationsContactUpdate'
- authorize :admin_contact
+ authorize :admin_crm_contact
field :contact,
Types::CustomerRelations::ContactType,
@@ -48,8 +48,6 @@ module Mutations
raise_resource_not_available_error! unless contact
group = contact.group
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, group, default_enabled: :yaml)
-
authorize!(group)
result = ::CustomerRelations::Contacts::UpdateService.new(group: group, current_user: current_user, params: args).execute(contact)
diff --git a/app/graphql/mutations/customer_relations/organizations/create.rb b/app/graphql/mutations/customer_relations/organizations/create.rb
index bb02e1f7346..17e0e9ad459 100644
--- a/app/graphql/mutations/customer_relations/organizations/create.rb
+++ b/app/graphql/mutations/customer_relations/organizations/create.rb
@@ -33,13 +33,11 @@ module Mutations
required: false,
description: 'Description of or notes for the organization.'
- authorize :admin_organization
+ authorize :admin_crm_organization
def resolve(args)
group = authorized_find!(id: args[:group_id])
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, group, default_enabled: :yaml)
-
result = ::CustomerRelations::Organizations::CreateService.new(group: group, current_user: current_user, params: args).execute
{ organization: result.payload, errors: result.errors }
end
diff --git a/app/graphql/mutations/customer_relations/organizations/update.rb b/app/graphql/mutations/customer_relations/organizations/update.rb
index d8eb55d77e9..21fcf565239 100644
--- a/app/graphql/mutations/customer_relations/organizations/update.rb
+++ b/app/graphql/mutations/customer_relations/organizations/update.rb
@@ -8,7 +8,7 @@ module Mutations
graphql_name 'CustomerRelationsOrganizationUpdate'
- authorize :admin_organization
+ authorize :admin_crm_organization
field :organization,
Types::CustomerRelations::OrganizationType,
@@ -39,8 +39,6 @@ module Mutations
raise_resource_not_available_error! unless organization
group = organization.group
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, group, default_enabled: :yaml)
-
authorize!(group)
result = ::CustomerRelations::Organizations::UpdateService.new(group: group, current_user: current_user, params: args).execute(organization)
diff --git a/app/graphql/mutations/issues/create.rb b/app/graphql/mutations/issues/create.rb
index 70a8f539ccf..72b03cc27c2 100644
--- a/app/graphql/mutations/issues/create.rb
+++ b/app/graphql/mutations/issues/create.rb
@@ -3,13 +3,14 @@
module Mutations
module Issues
class Create < BaseMutation
+ include Mutations::SpamProtection
include FindsProject
+ include CommonMutationArguments
+
graphql_name 'CreateIssue'
authorize :create_issue
- include CommonMutationArguments
-
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project full path the issue is associated with.'
@@ -76,9 +77,7 @@ module Mutations
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
issue = ::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
- if issue.spam?
- issue.errors.add(:base, 'Spam detected.')
- end
+ check_spam_action_response!(issue)
{
issue: issue.valid? ? issue : nil,
diff --git a/app/graphql/mutations/issues/set_crm_contacts.rb b/app/graphql/mutations/issues/set_crm_contacts.rb
new file mode 100644
index 00000000000..7a9e6237eaa
--- /dev/null
+++ b/app/graphql/mutations/issues/set_crm_contacts.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Issues
+ class SetCrmContacts < Base
+ graphql_name 'IssueSetCrmContacts'
+
+ argument :crm_contact_ids,
+ [::Types::GlobalIDType[::CustomerRelations::Contact]],
+ required: true,
+ description: 'Customer relations contact IDs to set. Replaces existing contacts by default.'
+
+ argument :operation_mode,
+ Types::MutationOperationModeEnum,
+ required: false,
+ description: 'Changes the operation mode. Defaults to REPLACE.'
+
+ def resolve(project_path:, iid:, crm_contact_ids:, operation_mode: Types::MutationOperationModeEnum.enum[:replace])
+ issue = authorized_find!(project_path: project_path, iid: iid)
+ project = issue.project
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, project.group, default_enabled: :yaml)
+
+ crm_contact_ids = crm_contact_ids.compact.map do |crm_contact_id|
+ raise Gitlab::Graphql::Errors::ArgumentError, "Contact #{crm_contact_id} is invalid." unless crm_contact_id.respond_to?(:model_id)
+
+ crm_contact_id.model_id.to_i
+ end
+
+ attribute_name = case operation_mode
+ when Types::MutationOperationModeEnum.enum[:append]
+ :add_crm_contact_ids
+ when Types::MutationOperationModeEnum.enum[:remove]
+ :remove_crm_contact_ids
+ else
+ :crm_contact_ids
+ end
+
+ response = ::Issues::SetCrmContactsService.new(project: project, current_user: current_user, params: { attribute_name => crm_contact_ids })
+ .execute(issue)
+
+ {
+ issue: issue,
+ errors: response.errors
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/set_wip.rb b/app/graphql/mutations/merge_requests/set_wip.rb
deleted file mode 100644
index 9b6b67d4b4f..00000000000
--- a/app/graphql/mutations/merge_requests/set_wip.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Mutations
- module MergeRequests
- class SetWip < Base
- graphql_name 'MergeRequestSetWip'
-
- argument :wip,
- GraphQL::Types::Boolean,
- required: true,
- description: <<~DESC
- Whether or not to set the merge request as a draft.
- DESC
-
- def resolve(project_path:, iid:, wip: nil)
- merge_request = authorized_find!(project_path: project_path, iid: iid)
- project = merge_request.project
-
- ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { wip_event: wip_event(merge_request, wip) })
- .execute(merge_request)
-
- {
- merge_request: merge_request,
- errors: errors_on_object(merge_request)
- }
- end
-
- private
-
- def wip_event(merge_request, wip)
- wip ? 'wip' : 'unwip'
- end
- end
- end
-end
diff --git a/app/graphql/mutations/merge_requests/toggle_attention_requested.rb b/app/graphql/mutations/merge_requests/toggle_attention_requested.rb
new file mode 100644
index 00000000000..f316f23fb85
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/toggle_attention_requested.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Mutations
+ module MergeRequests
+ class ToggleAttentionRequested < Base
+ graphql_name 'MergeRequestToggleAttentionRequested'
+
+ argument :user_id, ::Types::GlobalIDType[::User],
+ loads: Types::UserType,
+ required: true,
+ description: <<~DESC
+ User ID for the user to toggle attention requested.
+ DESC
+
+ def resolve(project_path:, iid:, user:)
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+
+ result = ::MergeRequests::ToggleAttentionRequestedService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request, user: user).execute
+
+ {
+ merge_request: merge_request,
+ errors: Array(result[:message])
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/security/ci_configuration/configure_sast_iac.rb b/app/graphql/mutations/security/ci_configuration/configure_sast_iac.rb
new file mode 100644
index 00000000000..a2073f10b1d
--- /dev/null
+++ b/app/graphql/mutations/security/ci_configuration/configure_sast_iac.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Security
+ module CiConfiguration
+ class ConfigureSastIac < BaseSecurityAnalyzer
+ graphql_name 'ConfigureSastIac'
+ description <<~DESC
+ Enable SAST IaC for a project in a new or
+ modified `.gitlab-ci.yml` file in a new branch. The new
+ branch and a URL to create a merge request are a part of the
+ response.
+ DESC
+
+ def configure_analyzer(project, **_args)
+ ::Security::CiConfiguration::SastIacCreateService.new(project, current_user).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/queries/epic/epic_children.query.graphql b/app/graphql/queries/epic/epic_children.query.graphql
index b0e55811b7d..be82813dddb 100644
--- a/app/graphql/queries/epic/epic_children.query.graphql
+++ b/app/graphql/queries/epic/epic_children.query.graphql
@@ -89,6 +89,7 @@ query childItems(
iid
epicIssueId
title
+ blocked
closedAt
state
createdAt
diff --git a/app/graphql/resolvers/base_issues_resolver.rb b/app/graphql/resolvers/base_issues_resolver.rb
new file mode 100644
index 00000000000..54ebb697cb2
--- /dev/null
+++ b/app/graphql/resolvers/base_issues_resolver.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class BaseIssuesResolver < BaseResolver
+ prepend IssueResolverArguments
+
+ argument :state, Types::IssuableStateEnum,
+ required: false,
+ description: 'Current state of this issue.'
+ argument :sort, Types::IssueSortEnum,
+ description: 'Sort issues by this criteria.',
+ required: false,
+ default_value: :created_desc
+
+ type Types::IssueType.connection_type, null: true
+
+ NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc
+ popularity_asc popularity_desc
+ label_priority_asc label_priority_desc
+ milestone_due_asc milestone_due_desc].freeze
+
+ def continue_issue_resolve(parent, finder, **args)
+ issues = Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all { |q| apply_lookahead(q) }
+
+ if non_stable_cursor_sort?(args[:sort])
+ # Certain complex sorts are not supported by the stable cursor pagination yet.
+ # In these cases, we use offset pagination, so we return the correct connection.
+ offset_pagination(issues)
+ else
+ issues
+ end
+ end
+
+ private
+
+ def unconditional_includes
+ [
+ {
+ project: [:project_feature]
+ },
+ :author
+ ]
+ end
+
+ def preloads
+ {
+ alert_management_alert: [:alert_management_alert],
+ labels: [:labels],
+ assignees: [:assignees],
+ timelogs: [:timelogs],
+ customer_relations_contacts: { customer_relations_contacts: [:group] }
+ }
+ end
+
+ def non_stable_cursor_sort?(sort)
+ NON_STABLE_CURSOR_SORTS.include?(sort)
+ end
+ end
+end
+
+Resolvers::BaseIssuesResolver.prepend_mod_with('Resolvers::BaseIssuesResolver')
diff --git a/app/graphql/resolvers/ci/pipeline_job_artifacts_resolver.rb b/app/graphql/resolvers/ci/pipeline_job_artifacts_resolver.rb
new file mode 100644
index 00000000000..35d30827561
--- /dev/null
+++ b/app/graphql/resolvers/ci/pipeline_job_artifacts_resolver.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class PipelineJobArtifactsResolver < BaseResolver
+ type [Types::Ci::JobArtifactType], null: false
+
+ alias_method :pipeline, :object
+
+ def resolve
+ find_job_artifacts
+ end
+
+ private
+
+ def find_job_artifacts
+ BatchLoader::GraphQL.for(pipeline).batch do |pipelines, loader|
+ ActiveRecord::Associations::Preloader.new.preload(pipelines, :job_artifacts) # rubocop: disable CodeReuse/ActiveRecord
+
+ pipelines.each { |pl| loader.call(pl, pl.job_artifacts) }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
index 855877110e5..38c79ff52ac 100644
--- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
+++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
@@ -55,8 +55,8 @@ module IssueResolverArguments
description: 'Filter issues by the given issue types.',
required: false
argument :milestone_wildcard_id, ::Types::MilestoneWildcardIdEnum,
- required: false,
- description: 'Filter issues by milestone ID wildcard.'
+ required: false,
+ description: 'Filter issues by milestone ID wildcard.'
argument :my_reaction_emoji, GraphQL::Types::String,
required: false,
description: 'Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported.'
@@ -83,6 +83,7 @@ module IssueResolverArguments
args[:attempt_project_search_optimizations] = true if args[:search].present?
prepare_assignee_username_params(args)
+ prepare_release_tag_params(args)
finder = IssuesFinder.new(current_user, args)
@@ -93,6 +94,7 @@ module IssueResolverArguments
params_not_mutually_exclusive(args, mutually_exclusive_assignee_username_args)
params_not_mutually_exclusive(args, mutually_exclusive_milestone_args)
params_not_mutually_exclusive(args.fetch(:not, {}), mutually_exclusive_milestone_args)
+ params_not_mutually_exclusive(args, mutually_exclusive_release_tag_args)
validate_anonymous_search_access! if args[:search].present?
super
@@ -105,10 +107,30 @@ module IssueResolverArguments
complexity
end
+
+ def accept_release_tag
+ argument :release_tag, [GraphQL::Types::String],
+ required: false,
+ description: "Release tag associated with the issue's milestone."
+ argument :release_tag_wildcard_id, Types::ReleaseTagWildcardIdEnum,
+ required: false,
+ description: 'Filter issues by release tag ID wildcard.'
+ end
end
private
+ def prepare_release_tag_params(args)
+ release_tag_wildcard = args.delete(:release_tag_wildcard_id)
+ return if release_tag_wildcard.blank?
+
+ args[:release_tag] ||= release_tag_wildcard
+ end
+
+ def mutually_exclusive_release_tag_args
+ [:release_tag, :release_tag_wildcard_id]
+ end
+
def prepare_assignee_username_params(args)
args[:assignee_username] = args.delete(:assignee_usernames) if args[:assignee_usernames].present?
args[:not][:assignee_username] = args[:not].delete(:assignee_usernames) if args.dig(:not, :assignee_usernames).present?
diff --git a/app/graphql/resolvers/concerns/resolves_groups.rb b/app/graphql/resolvers/concerns/resolves_groups.rb
new file mode 100644
index 00000000000..c451d4e7936
--- /dev/null
+++ b/app/graphql/resolvers/concerns/resolves_groups.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+# Mixin for all resolver classes for type `Types::GroupType.connection_type`.
+module ResolvesGroups
+ extend ActiveSupport::Concern
+ include LooksAhead
+
+ def resolve_with_lookahead(**args)
+ apply_lookahead(resolve_groups(**args))
+ end
+
+ private
+
+ # The resolver should implement this method.
+ def resolve_groups(**args)
+ raise NotImplementedError
+ end
+
+ def preloads
+ {
+ contacts: [:contacts],
+ container_repositories_count: [:container_repositories],
+ custom_emoji: [:custom_emoji],
+ full_path: [:route],
+ organizations: [:organizations],
+ path: [:route],
+ dependency_proxy_blob_count: [:dependency_proxy_blobs],
+ dependency_proxy_blobs: [:dependency_proxy_blobs],
+ dependency_proxy_image_count: [:dependency_proxy_manifests],
+ dependency_proxy_image_ttl_policy: [:dependency_proxy_image_ttl_policy],
+ dependency_proxy_setting: [:dependency_proxy_setting]
+ }
+ end
+end
diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb
index 7fb0852b11e..1c01e5e0250 100644
--- a/app/graphql/resolvers/concerns/resolves_pipelines.rb
+++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb
@@ -9,6 +9,9 @@ module ResolvesPipelines
Types::Ci::PipelineStatusEnum,
required: false,
description: "Filter pipelines by their status."
+ argument :scope, ::Types::Ci::PipelineScopeEnum,
+ required: false,
+ description: 'Filter pipelines by scope.'
argument :ref,
GraphQL::Types::String,
required: false,
diff --git a/app/graphql/resolvers/group_issues_resolver.rb b/app/graphql/resolvers/group_issues_resolver.rb
index 1db0ab08e31..28f9266974f 100644
--- a/app/graphql/resolvers/group_issues_resolver.rb
+++ b/app/graphql/resolvers/group_issues_resolver.rb
@@ -1,10 +1,18 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from IssuesResolver)
+# rubocop:disable Graphql/ResolverType (inherited from BaseIssuesResolver)
module Resolvers
- class GroupIssuesResolver < IssuesResolver
+ class GroupIssuesResolver < BaseIssuesResolver
include GroupIssuableResolver
include_subgroups 'issues'
+
+ def ready?(**args)
+ if args.dig(:not, :release_tag).present?
+ raise ::Gitlab::Graphql::Errors::ArgumentError, 'releaseTag filter is not allowed when parent is a group.'
+ end
+
+ super
+ end
end
end
diff --git a/app/graphql/resolvers/groups_resolver.rb b/app/graphql/resolvers/groups_resolver.rb
index b090fdc49d4..abd3bf9e6e0 100644
--- a/app/graphql/resolvers/groups_resolver.rb
+++ b/app/graphql/resolvers/groups_resolver.rb
@@ -2,6 +2,8 @@
module Resolvers
class GroupsResolver < BaseResolver
+ include ResolvesGroups
+
type Types::GroupType, null: true
argument :include_parent_descendants, GraphQL::Types::Boolean,
@@ -19,16 +21,12 @@ module Resolvers
alias_method :parent, :object
- def resolve(**args)
- return [] unless parent.present?
-
- find_groups(args)
- end
-
private
# rubocop: disable CodeReuse/ActiveRecord
- def find_groups(args)
+ def resolve_groups(args)
+ return Group.none unless parent.present?
+
GroupsFinder
.new(context[:current_user], args.merge(parent: parent))
.execute
diff --git a/app/graphql/resolvers/issue_status_counts_resolver.rb b/app/graphql/resolvers/issue_status_counts_resolver.rb
index 58cff559d0d..db5c91daac2 100644
--- a/app/graphql/resolvers/issue_status_counts_resolver.rb
+++ b/app/graphql/resolvers/issue_status_counts_resolver.rb
@@ -5,6 +5,7 @@ module Resolvers
prepend IssueResolverArguments
type Types::IssueStatusCountsType, null: true
+ accept_release_tag
extras [:lookahead]
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index b556964ae0c..4b52ef61d57 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -1,61 +1,8 @@
# frozen_string_literal: true
+# rubocop:disable Graphql/ResolverType (inherited from BaseIssuesResolver)
module Resolvers
- class IssuesResolver < BaseResolver
- prepend IssueResolverArguments
-
- argument :state, Types::IssuableStateEnum,
- required: false,
- description: 'Current state of this issue.'
- argument :sort, Types::IssueSortEnum,
- description: 'Sort issues by this criteria.',
- required: false,
- default_value: :created_desc
-
- type Types::IssueType.connection_type, null: true
-
- NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc
- popularity_asc popularity_desc
- label_priority_asc label_priority_desc
- milestone_due_asc milestone_due_desc].freeze
-
- def continue_issue_resolve(parent, finder, **args)
- issues = Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all { |q| apply_lookahead(q) }
-
- if non_stable_cursor_sort?(args[:sort])
- # Certain complex sorts are not supported by the stable cursor pagination yet.
- # In these cases, we use offset pagination, so we return the correct connection.
- offset_pagination(issues)
- else
- issues
- end
- end
-
- private
-
- def unconditional_includes
- [
- {
- project: [:project_feature]
- },
- :author
- ]
- end
-
- def preloads
- {
- alert_management_alert: [:alert_management_alert],
- labels: [:labels],
- assignees: [:assignees],
- timelogs: [:timelogs],
- customer_relations_contacts: { customer_relations_contacts: [:group] }
- }
- end
-
- def non_stable_cursor_sort?(sort)
- NON_STABLE_CURSOR_SORTS.include?(sort)
- end
+ class IssuesResolver < BaseIssuesResolver
+ accept_release_tag
end
end
-
-Resolvers::IssuesResolver.prepend_mod_with('Resolvers::IssuesResolver')
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
index c0dd61078c6..bd7f1f0774e 100644
--- a/app/graphql/resolvers/merge_requests_resolver.rb
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -68,6 +68,12 @@ module Resolvers
description: 'Sort merge requests by this criteria.',
required: false,
default_value: :created_desc
+ argument :created_after, Types::TimeType,
+ required: false,
+ description: 'Merge requests created after this timestamp.'
+ argument :created_before, Types::TimeType,
+ required: false,
+ description: 'Merge requests created before this timestamp.'
negated do
argument :labels, [GraphQL::Types::String],
diff --git a/app/graphql/resolvers/topics_resolver.rb b/app/graphql/resolvers/topics_resolver.rb
new file mode 100644
index 00000000000..d8199f3d89b
--- /dev/null
+++ b/app/graphql/resolvers/topics_resolver.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class TopicsResolver < BaseResolver
+ type Types::Projects::TopicType, null: true
+
+ argument :search, GraphQL::Types::String,
+ required: false,
+ description: 'Search query for topic name.'
+
+ def resolve(**args)
+ if args[:search].present?
+ ::Projects::Topic.search(args[:search]).order_by_total_projects_count
+ else
+ ::Projects::Topic.order_by_total_projects_count
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/users/groups_resolver.rb b/app/graphql/resolvers/users/groups_resolver.rb
index 0899b08e19c..eafb56d8f4c 100644
--- a/app/graphql/resolvers/users/groups_resolver.rb
+++ b/app/graphql/resolvers/users/groups_resolver.rb
@@ -3,8 +3,8 @@
module Resolvers
module Users
class GroupsResolver < BaseResolver
+ include ResolvesGroups
include Gitlab::Graphql::Authorize::AuthorizeResource
- include LooksAhead
type Types::GroupType.connection_type, null: true
@@ -20,22 +20,17 @@ module Resolvers
description: 'Filter by permissions the user has on groups.'
before_connection_authorization do |nodes, current_user|
- Preloaders::UserMaxAccessLevelInGroupsPreloader.new(nodes, current_user).execute
+ Preloaders::GroupPolicyPreloader.new(nodes, current_user).execute
end
- def resolve_with_lookahead(**args)
- return unless Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
-
- apply_lookahead(Groups::UserGroupsFinder.new(current_user, object, args).execute)
+ def ready?(**args)
+ Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
end
private
- def preloads
- {
- path: [:route],
- full_path: [:route]
- }
+ def resolve_groups(**args)
+ Groups::UserGroupsFinder.new(current_user, object, args).execute
end
end
end
diff --git a/app/graphql/types/ci/job_artifact_type.rb b/app/graphql/types/ci/job_artifact_type.rb
index f80681bcf36..69bb5325dba 100644
--- a/app/graphql/types/ci/job_artifact_type.rb
+++ b/app/graphql/types/ci/job_artifact_type.rb
@@ -12,6 +12,10 @@ module Types
field :file_type, ::Types::Ci::JobArtifactFileTypeEnum, null: true,
description: 'File type of the artifact.'
+ field :name, GraphQL::Types::String, null: true,
+ description: 'File name of the artifact.',
+ method: :filename
+
def download_path
::Gitlab::Routing.url_helpers.download_project_job_artifacts_path(
object.project,
diff --git a/app/graphql/types/ci/pipeline_scope_enum.rb b/app/graphql/types/ci/pipeline_scope_enum.rb
new file mode 100644
index 00000000000..3ce81bc7bf3
--- /dev/null
+++ b/app/graphql/types/ci/pipeline_scope_enum.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class PipelineScopeEnum < BaseEnum
+ SCOPES_DESCRIPTION = {
+ running: 'Pipeline is running.',
+ pending: 'Pipeline has not started running yet.',
+ finished: 'Pipeline has completed.',
+ branches: 'Branches.',
+ tags: 'Tags.'
+ }.freeze
+
+ SCOPES_DESCRIPTION.each do |scope, description|
+ value scope.to_s.upcase,
+ description: description,
+ value: scope.to_s
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb
index 2800454a999..c8e031e18ea 100644
--- a/app/graphql/types/ci/pipeline_status_enum.rb
+++ b/app/graphql/types/ci/pipeline_status_enum.rb
@@ -3,10 +3,24 @@
module Types
module Ci
class PipelineStatusEnum < BaseEnum
- ::Ci::Pipeline.all_state_names.each do |state_symbol|
- value state_symbol.to_s.upcase,
- description: "#{::Ci::Pipeline::STATUSES_DESCRIPTION[state_symbol]}.",
- value: state_symbol.to_s
+ STATUSES_DESCRIPTION = {
+ created: 'Pipeline has been created.',
+ waiting_for_resource: 'A resource (for example, a runner) that the pipeline requires to run is unavailable.',
+ preparing: 'Pipeline is preparing to run.',
+ pending: 'Pipeline has not started running yet.',
+ running: 'Pipeline is running.',
+ failed: 'At least one stage of the pipeline failed.',
+ success: 'Pipeline completed successfully.',
+ canceled: 'Pipeline was canceled before completion.',
+ skipped: 'Pipeline was skipped.',
+ manual: 'Pipeline needs to be manually started.',
+ scheduled: 'Pipeline is scheduled to run.'
+ }.freeze
+
+ STATUSES_DESCRIPTION.each do |state, description|
+ value state.to_s.upcase,
+ description: description,
+ value: state.to_s
end
end
end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 493ce188d9b..da2f11be9e2 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -108,6 +108,11 @@ module Types
description: 'Name of the job.'
end
+ field :job_artifacts,
+ null: true,
+ description: 'Job artifacts of the pipeline.',
+ resolver: ::Resolvers::Ci::PipelineJobArtifactsResolver
+
field :source_job,
type: Types::Ci::JobType,
null: true,
@@ -125,6 +130,10 @@ module Types
field :path, GraphQL::Types::String, null: true,
description: "Relative path to the pipeline's page."
+ field :commit, Types::CommitType, null: true,
+ description: "Git commit of the pipeline.",
+ calls_gitaly: true
+
field :commit_path, GraphQL::Types::String, null: true,
description: 'Path to the commit that triggered the pipeline.'
diff --git a/app/graphql/types/ci/runner_setup_type.rb b/app/graphql/types/ci/runner_setup_type.rb
index 70f33cef8ad..b6b020db40e 100644
--- a/app/graphql/types/ci/runner_setup_type.rb
+++ b/app/graphql/types/ci/runner_setup_type.rb
@@ -9,7 +9,7 @@ module Types
field :install_instructions, GraphQL::Types::String, null: false,
description: 'Instructions for installing the runner on the specified architecture.'
field :register_instructions, GraphQL::Types::String, null: true,
- description: 'Instructions for registering the runner.'
+ description: 'Instructions for registering the runner. The actual registration tokens are not included in the commands. Instead, a placeholder `$REGISTRATION_TOKEN` is shown.'
end
end
end
diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb
index dc1bf6a3101..7d141bd6daa 100644
--- a/app/graphql/types/commit_type.rb
+++ b/app/graphql/types/commit_type.rb
@@ -14,12 +14,19 @@ module Types
description: 'SHA1 ID of the commit.'
field :short_id, type: GraphQL::Types::String, null: false,
description: 'Short SHA1 ID of the commit.'
+
field :title, type: GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Title of the commit message.'
markdown_field :title_html, null: true
+
+ field :full_title, type: GraphQL::Types::String, null: true, calls_gitaly: true,
+ description: 'Full title of the commit message.'
+ markdown_field :full_title_html, null: true
+
field :description, type: GraphQL::Types::String, null: true,
description: 'Description of the commit message.'
markdown_field :description_html, null: true
+
field :message, type: GraphQL::Types::String, null: true,
description: 'Raw commit message.'
field :authored_date, type: Types::TimeType, null: true,
diff --git a/app/graphql/types/customer_relations/contact_type.rb b/app/graphql/types/customer_relations/contact_type.rb
index b5224a3e239..6a3882c66af 100644
--- a/app/graphql/types/customer_relations/contact_type.rb
+++ b/app/graphql/types/customer_relations/contact_type.rb
@@ -5,7 +5,7 @@ module Types
class ContactType < BaseObject
graphql_name 'CustomerRelationsContact'
- authorize :read_contact
+ authorize :read_crm_contact
field :id,
GraphQL::Types::ID,
diff --git a/app/graphql/types/customer_relations/organization_type.rb b/app/graphql/types/customer_relations/organization_type.rb
index 9b22fa35b11..0f97f0a2433 100644
--- a/app/graphql/types/customer_relations/organization_type.rb
+++ b/app/graphql/types/customer_relations/organization_type.rb
@@ -5,7 +5,7 @@ module Types
class OrganizationType < BaseObject
graphql_name 'CustomerRelationsOrganization'
- authorize :read_organization
+ authorize :read_crm_organization
field :id,
GraphQL::Types::ID,
diff --git a/app/graphql/types/dependency_proxy/manifest_type.rb b/app/graphql/types/dependency_proxy/manifest_type.rb
index 9aa62266ef7..ef9f730df43 100644
--- a/app/graphql/types/dependency_proxy/manifest_type.rb
+++ b/app/graphql/types/dependency_proxy/manifest_type.rb
@@ -8,6 +8,7 @@ module Types
authorize :read_dependency_proxy
+ field :id, ::Types::GlobalIDType[::DependencyProxy::Manifest], null: false, description: 'ID of the manifest.'
field :created_at, Types::TimeType, null: false, description: 'Date of creation.'
field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
field :file_name, GraphQL::Types::String, null: false, description: 'Name of the manifest.'
diff --git a/app/graphql/types/evidence_type.rb b/app/graphql/types/evidence_type.rb
index 26fb64d25d8..33f46c712f1 100644
--- a/app/graphql/types/evidence_type.rb
+++ b/app/graphql/types/evidence_type.rb
@@ -5,7 +5,7 @@ module Types
graphql_name 'ReleaseEvidence'
description 'Evidence for a release'
- authorize :download_code
+ authorize :read_release_evidence
present_using Releases::EvidencePresenter
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index b1bbabcdaed..4a20d84f2ab 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -34,6 +34,7 @@ module Types
null: true,
method: :project_creation_level_str,
description: 'Permission level required to create projects in the group.'
+
field :subgroup_creation_level,
type: GraphQL::Types::String,
null: true,
@@ -44,6 +45,7 @@ module Types
type: GraphQL::Types::Boolean,
null: true,
description: 'Indicates if all users in this group are required to set up two-factor authentication.'
+
field :two_factor_grace_period,
type: GraphQL::Types::Int,
null: true,
@@ -220,12 +222,16 @@ module Types
group.container_repositories.size
end
+ def dependency_proxy_manifests
+ group.dependency_proxy_manifests.order_id_desc
+ end
+
def dependency_proxy_image_count
- group.dependency_proxy_manifests.count
+ group.dependency_proxy_manifests.size
end
def dependency_proxy_blob_count
- group.dependency_proxy_blobs.count
+ group.dependency_proxy_blobs.size
end
def dependency_proxy_total_size
diff --git a/app/graphql/types/issues/negated_issue_filter_input_type.rb b/app/graphql/types/issues/negated_issue_filter_input_type.rb
index c8b7cdaa68e..73e090a4802 100644
--- a/app/graphql/types/issues/negated_issue_filter_input_type.rb
+++ b/app/graphql/types/issues/negated_issue_filter_input_type.rb
@@ -14,6 +14,9 @@ module Types
argument :milestone_title, [GraphQL::Types::String],
required: false,
description: 'Milestone not applied to this issue.'
+ argument :release_tag, [GraphQL::Types::String],
+ required: false,
+ description: "Release tag not associated with the issue's milestone. Ignored when parent is a group."
argument :author_username, GraphQL::Types::String,
required: false,
description: "Username of a user who didn't author the issue."
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 004ac364487..a0f00ddc3c6 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -53,9 +53,6 @@ module Types
description: 'Indicates if the source branch is protected.'
field :target_branch, GraphQL::Types::String, null: false,
description: 'Target branch of the merge request.'
- field :work_in_progress, GraphQL::Types::Boolean, method: :work_in_progress?, null: false,
- deprecated: { reason: 'Use `draft`', milestone: '13.12' },
- description: 'Indicates if the merge request is a draft.'
field :draft, GraphQL::Types::Boolean, method: :draft?, null: false,
description: 'Indicates if the merge request is a draft.'
field :merge_when_pipeline_succeeds, GraphQL::Types::Boolean, null: true,
@@ -102,7 +99,8 @@ module Types
field :default_merge_commit_message, GraphQL::Types::String, null: true,
description: 'Default merge commit message of the merge request.'
field :default_merge_commit_message_with_description, GraphQL::Types::String, null: true,
- description: 'Default merge commit message of the merge request with description.'
+ description: 'Default merge commit message of the merge request with description. Will have the same value as `defaultMergeCommitMessage` when project has `mergeCommitTemplate` set.',
+ deprecated: { reason: 'Define merge commit template in project and use `defaultMergeCommitMessage`', milestone: '14.5' }
field :default_squash_commit_message, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Default squash commit message of the merge request.'
field :merge_ongoing, GraphQL::Types::Boolean, method: :merge_ongoing?, null: false,
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index cd4c45d2942..e8a952e9c61 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -16,6 +16,7 @@ module Types
mount_mutation Mutations::AlertManagement::HttpIntegration::ResetToken
mount_mutation Mutations::AlertManagement::HttpIntegration::Destroy
mount_mutation Mutations::Security::CiConfiguration::ConfigureSast
+ mount_mutation Mutations::Security::CiConfiguration::ConfigureSastIac
mount_mutation Mutations::Security::CiConfiguration::ConfigureSecretDetection
mount_mutation Mutations::AlertManagement::PrometheusIntegration::Create
mount_mutation Mutations::AlertManagement::PrometheusIntegration::Update
@@ -48,6 +49,7 @@ module Types
mount_mutation Mutations::Environments::CanaryIngress::Update
mount_mutation Mutations::Issues::Create
mount_mutation Mutations::Issues::SetAssignees
+ mount_mutation Mutations::Issues::SetCrmContacts
mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetLocked
mount_mutation Mutations::Issues::SetDueDate
@@ -63,12 +65,10 @@ module Types
mount_mutation Mutations::MergeRequests::SetLocked
mount_mutation Mutations::MergeRequests::SetMilestone
mount_mutation Mutations::MergeRequests::SetSubscription
- mount_mutation Mutations::MergeRequests::SetWip,
- calls_gitaly: true,
- deprecated: { reason: 'Use mergeRequestSetDraft', milestone: '13.12' }
mount_mutation Mutations::MergeRequests::SetDraft, calls_gitaly: true
mount_mutation Mutations::MergeRequests::SetAssignees
mount_mutation Mutations::MergeRequests::ReviewerRereview
+ mount_mutation Mutations::MergeRequests::ToggleAttentionRequested, feature_flag: :mr_attention_requests
mount_mutation Mutations::Metrics::Dashboard::Annotations::Create
mount_mutation Mutations::Metrics::Dashboard::Annotations::Delete
mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true
diff --git a/app/graphql/types/packages/file_metadata_type.rb b/app/graphql/types/packages/file_metadata_type.rb
index 46ccb424218..0731fc897a2 100644
--- a/app/graphql/types/packages/file_metadata_type.rb
+++ b/app/graphql/types/packages/file_metadata_type.rb
@@ -14,6 +14,8 @@ module Types
case object
when ::Packages::Conan::FileMetadatum
::Types::Packages::Conan::FileMetadatumType
+ when ::Packages::Helm::FileMetadatum
+ ::Types::Packages::Helm::FileMetadatumType
else
# NOTE: This method must be kept in sync with `PackageFileType#file_metadata`,
# which must never produce data that this discriminator cannot handle.
@@ -21,7 +23,8 @@ module Types
end
end
- orphan_types Types::Packages::Conan::FileMetadatumType
+ orphan_types Types::Packages::Conan::FileMetadatumType,
+ Types::Packages::Helm::FileMetadatumType
end
end
end
diff --git a/app/graphql/types/packages/helm/dependency_type.rb b/app/graphql/types/packages/helm/dependency_type.rb
new file mode 100644
index 00000000000..35598c2b1d7
--- /dev/null
+++ b/app/graphql/types/packages/helm/dependency_type.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Helm
+ # rubocop: disable Graphql/AuthorizeTypes
+ class DependencyType < BaseObject
+ graphql_name 'PackageHelmDependencyType'
+ description 'Represents a Helm dependency'
+
+ # Need to be synced with app/validators/json_schemas/helm_metadata.json#dependencies
+ field :name, GraphQL::Types::String, null: true, description: 'Name of the dependency.'
+ field :version, GraphQL::Types::String, null: true, description: 'Version of the dependency.'
+ field :repository, GraphQL::Types::String, null: true, description: 'Repository of the dependency.'
+ field :condition, GraphQL::Types::String, null: true, description: 'Condition of the dependency.'
+ field :tags, [GraphQL::Types::String], null: true, description: 'Tags of the dependency.'
+ field :enabled, GraphQL::Types::Boolean, null: true, description: 'Indicates the dependency is enabled.'
+ field :import_values, [GraphQL::Types::JSON], null: true, description: 'Import-values of the dependency.', hash_key: "import-values" # rubocop:disable Graphql/JSONType
+ field :alias, GraphQL::Types::String, null: true, description: 'Alias of the dependency.', resolver_method: :resolve_alias
+
+ # field :alias` conflicts with a built-in method
+ def resolve_alias
+ object['alias']
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/helm/file_metadatum_type.rb b/app/graphql/types/packages/helm/file_metadatum_type.rb
new file mode 100644
index 00000000000..734ed52df40
--- /dev/null
+++ b/app/graphql/types/packages/helm/file_metadatum_type.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Helm
+ class FileMetadatumType < BaseObject
+ graphql_name 'HelmFileMetadata'
+ description 'Helm file metadata'
+
+ implements Types::Packages::FileMetadataType
+
+ authorize :read_package
+
+ field :channel, GraphQL::Types::String, null: false, description: 'Channel of the Helm chart.'
+ field :metadata, Types::Packages::Helm::MetadataType, null: false, description: 'Metadata of the Helm chart.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/helm/maintainer_type.rb b/app/graphql/types/packages/helm/maintainer_type.rb
new file mode 100644
index 00000000000..6d25a26c46b
--- /dev/null
+++ b/app/graphql/types/packages/helm/maintainer_type.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Helm
+ # rubocop: disable Graphql/AuthorizeTypes
+ class MaintainerType < BaseObject
+ graphql_name 'PackageHelmMaintainerType'
+ description 'Represents a Helm maintainer'
+
+ # Need to be synced with app/validators/json_schemas/helm_metadata.json#maintainers
+ field :name, GraphQL::Types::String, null: true, description: 'Name of the maintainer.'
+ field :email, GraphQL::Types::String, null: true, description: 'Email of the maintainer.'
+ field :url, GraphQL::Types::String, null: true, description: 'URL of the maintainer.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/helm/metadata_type.rb b/app/graphql/types/packages/helm/metadata_type.rb
new file mode 100644
index 00000000000..eeb3e8087a8
--- /dev/null
+++ b/app/graphql/types/packages/helm/metadata_type.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Helm
+ # rubocop: disable Graphql/AuthorizeTypes
+ class MetadataType < BaseObject
+ graphql_name 'PackageHelmMetadataType'
+ description 'Represents the contents of a Helm Chart.yml file'
+
+ # Need to be synced with app/validators/json_schemas/helm_metadata.json
+ field :name, GraphQL::Types::String, null: false, description: 'Name of the chart.'
+ field :home, GraphQL::Types::String, null: true, description: 'URL of the home page.'
+ field :sources, [GraphQL::Types::String], null: true, description: 'URLs of the source code for the chart.'
+ field :version, GraphQL::Types::String, null: false, description: 'Version of the chart.'
+ field :description, GraphQL::Types::String, null: true, description: 'Description of the chart.'
+ field :keywords, [GraphQL::Types::String], null: true, description: 'Keywords for the chart.'
+ field :maintainers, [Types::Packages::Helm::MaintainerType], null: true, description: 'Maintainers of the chart.'
+ field :icon, GraphQL::Types::String, null: true, description: 'URL to an SVG or PNG image for the chart.'
+ field :api_version, GraphQL::Types::String, null: false, description: 'API version of the chart.', hash_key: "apiVersion"
+ field :condition, GraphQL::Types::String, null: true, description: 'Condition for the chart.'
+ field :tags, GraphQL::Types::String, null: true, description: 'Tags for the chart.'
+ field :app_version, GraphQL::Types::String, null: true, description: 'App version of the chart.', hash_key: "appVersion"
+ field :deprecated, GraphQL::Types::Boolean, null: true, description: 'Indicates if the chart is deprecated.'
+ field :annotations, GraphQL::Types::JSON, null: true, description: 'Annotations for the chart.' # rubocop:disable Graphql/JSONType
+ field :kube_version, GraphQL::Types::String, null: true, description: 'Kubernetes versions for the chart.', hash_key: "kubeVersion"
+ field :dependencies, [Types::Packages::Helm::DependencyType], null: true, description: 'Dependencies of the chart.'
+ field :type, GraphQL::Types::String, null: true, description: 'Type of the chart.', hash_key: "appVersion"
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_file_type.rb b/app/graphql/types/packages/package_file_type.rb
index 8cc0f9b984a..f90a0992bf8 100644
--- a/app/graphql/types/packages/package_file_type.rb
+++ b/app/graphql/types/packages/package_file_type.rb
@@ -27,6 +27,8 @@ module Types
case object.package.package_type
when 'conan'
object.conan_file_metadatum
+ when 'helm'
+ object.helm_file_metadatum
else
nil
end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 791875242df..b6cb9cd3302 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -306,7 +306,7 @@ module Types
null: true,
description: 'A single release of the project.',
resolver: Resolvers::ReleasesResolver.single,
- authorize: :download_code
+ authorize: :read_release
field :container_expiration_policy,
Types::ContainerExpirationPolicyType,
@@ -381,6 +381,11 @@ module Types
description: 'Cluster agents associated with the project.',
resolver: ::Resolvers::Clusters::AgentsResolver
+ field :merge_commit_template,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Template used to create merge commit message in merge requests.'
+
def label(title:)
BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
LabelsFinder
diff --git a/app/graphql/types/projects/topic_type.rb b/app/graphql/types/projects/topic_type.rb
new file mode 100644
index 00000000000..79ab69e794b
--- /dev/null
+++ b/app/graphql/types/projects/topic_type.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Types
+ module Projects
+ # rubocop: disable Graphql/AuthorizeTypes
+ class TopicType < BaseObject
+ graphql_name 'Topic'
+
+ field :id, GraphQL::Types::ID, null: false,
+ description: 'ID of the topic.'
+
+ field :name, GraphQL::Types::String, null: false,
+ description: 'Name of the topic.'
+
+ field :description, GraphQL::Types::String, null: true,
+ description: 'Description of the topic.'
+ markdown_field :description_html, null: true
+
+ field :avatar_url, GraphQL::Types::String, null: true,
+ description: 'URL to avatar image file of the topic.'
+
+ def avatar_url
+ object.avatar_url(only_path: false)
+ end
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index ed4ddbb982b..4a4d6727c3f 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -140,6 +140,11 @@ module Types
null: true,
resolver: Resolvers::BoardListResolver
+ field :topics, Types::Projects::TopicType.connection_type,
+ null: true,
+ resolver: Resolvers::TopicsResolver,
+ description: "Find project topics."
+
def design_management
DesignManagementObject.new(nil)
end
diff --git a/app/graphql/types/release_links_type.rb b/app/graphql/types/release_links_type.rb
index 7830e29f3cd..37ad52ce6d0 100644
--- a/app/graphql/types/release_links_type.rb
+++ b/app/graphql/types/release_links_type.rb
@@ -4,7 +4,7 @@ module Types
class ReleaseLinksType < BaseObject
graphql_name 'ReleaseLinks'
- authorize :download_code
+ authorize :read_release
alias_method :release, :object
@@ -16,14 +16,19 @@ module Types
description: "HTTP URL of the release's edit page.",
authorize: :update_release
field :opened_merge_requests_url, GraphQL::Types::String, null: true,
- description: 'HTTP URL of the merge request page, filtered by this release and `state=open`.'
+ description: 'HTTP URL of the merge request page, filtered by this release and `state=open`.',
+ authorize: :download_code
field :merged_merge_requests_url, GraphQL::Types::String, null: true,
- description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`.'
+ description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`.',
+ authorize: :download_code
field :closed_merge_requests_url, GraphQL::Types::String, null: true,
- description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`.'
+ description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`.',
+ authorize: :download_code
field :opened_issues_url, GraphQL::Types::String, null: true,
- description: 'HTTP URL of the issues page, filtered by this release and `state=open`.'
+ description: 'HTTP URL of the issues page, filtered by this release and `state=open`.',
+ authorize: :download_code
field :closed_issues_url, GraphQL::Types::String, null: true,
- description: 'HTTP URL of the issues page, filtered by this release and `state=closed`.'
+ description: 'HTTP URL of the issues page, filtered by this release and `state=closed`.',
+ authorize: :download_code
end
end
diff --git a/app/graphql/types/release_tag_wildcard_id_enum.rb b/app/graphql/types/release_tag_wildcard_id_enum.rb
new file mode 100644
index 00000000000..27765864101
--- /dev/null
+++ b/app/graphql/types/release_tag_wildcard_id_enum.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ class ReleaseTagWildcardIdEnum < BaseEnum
+ graphql_name 'ReleaseTagWildcardId'
+ description 'Release tag ID wildcard values'
+
+ value 'NONE', 'No release tag is assigned.'
+ value 'ANY', 'Release tag is assigned.'
+ end
+end
diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb
index 6dda93c7329..fcc9ec49252 100644
--- a/app/graphql/types/release_type.rb
+++ b/app/graphql/types/release_type.rb
@@ -14,8 +14,7 @@ module Types
present_using ReleasePresenter
field :tag_name, GraphQL::Types::String, null: true, method: :tag,
- description: 'Name of the tag associated with the release.',
- authorize: :download_code
+ description: 'Name of the tag associated with the release.'
field :tag_path, GraphQL::Types::String, null: true,
description: 'Relative web path to the tag associated with the release.',
authorize: :download_code
diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb
index ef7f535212f..104171e6772 100644
--- a/app/graphql/types/repository/blob_type.rb
+++ b/app/graphql/types/repository/blob_type.rb
@@ -68,6 +68,9 @@ module Types
field :replace_path, GraphQL::Types::String, null: true,
description: 'Web path to replace the blob content.'
+ field :pipeline_editor_path, GraphQL::Types::String, null: true,
+ description: 'Web path to edit .gitlab-ci.yml file.'
+
field :file_type, GraphQL::Types::String, null: true,
description: 'Expected format of the blob based on the extension.'
diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb
index 63d1eef5b59..fc9860900c9 100644
--- a/app/graphql/types/repository_type.rb
+++ b/app/graphql/types/repository_type.rb
@@ -16,8 +16,7 @@ module Types
description: 'Tree of the repository.'
field :paginated_tree, Types::Tree::TreeType.connection_type, null: true, resolver: Resolvers::PaginatedTreeResolver, calls_gitaly: true,
max_page_size: 100,
- description: 'Paginated tree of the repository.',
- feature_flag: :paginated_tree_graphql_query
+ description: 'Paginated tree of the repository.'
field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
description: 'Blobs contained within the repository'
field :branch_names, [GraphQL::Types::String], null: true, calls_gitaly: true,
diff --git a/app/helpers/admin/deploy_key_helper.rb b/app/helpers/admin/deploy_key_helper.rb
new file mode 100644
index 00000000000..caf3757a68e
--- /dev/null
+++ b/app/helpers/admin/deploy_key_helper.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Admin
+ module DeployKeyHelper
+ def admin_deploy_keys_data
+ {
+ edit_path: edit_admin_deploy_key_path(':id'),
+ delete_path: admin_deploy_key_path(':id'),
+ create_path: new_admin_deploy_key_path,
+ empty_state_svg_path: image_path('illustrations/empty-state/empty-deploy-keys-lg.svg')
+ }
+ end
+ end
+end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 60e37c96f61..5ca360f38da 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -39,14 +39,14 @@ module AppearancesHelper
if current_appearance&.header_logo?
image_tag current_appearance.header_logo_path, class: 'brand-header-logo'
else
- render 'shared/logo.svg'
+ render partial: 'shared/logo', formats: :svg
end
end
# Skip the 'GitLab' type logo when custom brand logo is set
def brand_header_logo_type
unless current_appearance&.header_logo?
- render 'shared/logo_type.svg'
+ render partial: 'shared/logo_type', formats: :svg
end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 2103a37180f..b8ee71daeee 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -404,6 +404,10 @@ module ApplicationSettingsHelper
:keep_latest_artifact,
:whats_new_variant,
:user_deactivation_emails_enabled,
+ :sentry_enabled,
+ :sentry_dsn,
+ :sentry_clientside_dsn,
+ :sentry_environment,
:sidekiq_job_limiter_mode,
:sidekiq_job_limiter_compression_threshold_bytes,
:sidekiq_job_limiter_limit_bytes,
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index a0c3a6f2f52..6fe92a5a978 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -8,6 +8,7 @@ module AuthHelper
azure_oauth2
bitbucket
facebook
+ dingtalk
github
gitlab
google_oauth2
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 33d5bae88f4..c26a73028b9 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -43,7 +43,7 @@ module BoardsHelper
def build_issue_link_base
if board.group_board?
- "#{group_path(@board.group)}/:project_path/issues"
+ "/:project_path/-/issues"
else
project_issues_path(@project)
end
diff --git a/app/helpers/breadcrumbs_helper.rb b/app/helpers/breadcrumbs_helper.rb
index ade7c48b03f..3a622a65685 100644
--- a/app/helpers/breadcrumbs_helper.rb
+++ b/app/helpers/breadcrumbs_helper.rb
@@ -27,10 +27,10 @@ module BreadcrumbsHelper
end
end
- def add_to_breadcrumb_dropdown(link, location: :before)
- @breadcrumb_dropdown_links ||= {}
- @breadcrumb_dropdown_links[location] ||= []
- @breadcrumb_dropdown_links[location] << link
+ def add_to_breadcrumb_collapsed_links(link, location: :before)
+ @breadcrumb_collapsed_links ||= {}
+ @breadcrumb_collapsed_links[location] ||= []
+ @breadcrumb_collapsed_links[location] << link
end
def push_to_schema_breadcrumb(text, link)
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
index 6be46b40023..6104a1256d5 100644
--- a/app/helpers/ci/pipelines_helper.rb
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -67,6 +67,17 @@ module Ci
]
end
+ def has_pipeline_badges?(pipeline)
+ pipeline.child? ||
+ pipeline.latest? ||
+ pipeline.merge_train_pipeline? ||
+ pipeline.has_yaml_errors? ||
+ pipeline.failure_reason? ||
+ pipeline.auto_devops_source? ||
+ pipeline.detached_merge_request_pipeline? ||
+ pipeline.stuck?
+ end
+
private
def warning_markdown(pipeline)
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
index ec10610714b..17057505173 100644
--- a/app/helpers/ci/runners_helper.rb
+++ b/app/helpers/ci/runners_helper.rb
@@ -6,27 +6,30 @@ module Ci
def runner_status_icon(runner, size: 16, icon_class: '')
status = runner.status
+ active = runner.active
title = ''
icon = 'warning-solid'
span_class = ''
case status
+ when :online
+ if active
+ title = s_("Runners|Runner is online, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) }
+ icon = 'status-active'
+ span_class = 'gl-text-green-500'
+ else
+ title = s_("Runners|Runner is paused, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) }
+ icon = 'status-paused'
+ span_class = 'gl-text-gray-600'
+ end
when :not_connected
title = s_("Runners|New runner, has not connected yet")
icon = 'warning-solid'
- when :online
- title = s_("Runners|Runner is online, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) }
- icon = 'status-active'
- span_class = 'gl-text-green-500'
when :offline
title = s_("Runners|Runner is offline, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) }
icon = 'status-failed'
span_class = 'gl-text-red-500'
- when :paused
- title = s_("Runners|Runner is paused, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) }
- icon = 'status-paused'
- span_class = 'gl-text-gray-600'
end
content_tag(:span, class: span_class, title: title, data: { toggle: 'tooltip', container: 'body', testid: 'runner_status_icon', qa_selector: "runner_status_#{status}_content" }) do
@@ -57,11 +60,30 @@ module Ci
end
end
+ def admin_runners_data_attributes
+ {
+ # Runner install help page is external, located at
+ # https://gitlab.com/gitlab-org/gitlab-runner
+ runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
+ registration_token: Gitlab::CurrentSettings.runners_registration_token,
+
+ # All runner counts are returned as formatted strings
+ active_runners_count: Ci::Runner.online.count.to_s,
+ all_runners_count: limited_counter_with_delimiter(Ci::Runner),
+ instance_runners_count: limited_counter_with_delimiter(Ci::Runner.instance_type),
+ group_runners_count: limited_counter_with_delimiter(Ci::Runner.group_type),
+ project_runners_count: limited_counter_with_delimiter(Ci::Runner.project_type)
+ }
+ end
+
def group_shared_runners_settings_data(group)
{
update_path: api_v4_groups_path(id: group.id),
shared_runners_availability: group.shared_runners_setting,
- parent_shared_runners_availability: group.parent&.shared_runners_setting
+ parent_shared_runners_availability: group.parent&.shared_runners_setting,
+ runner_enabled: Namespace::SR_ENABLED,
+ runner_disabled: Namespace::SR_DISABLED_AND_UNOVERRIDABLE,
+ runner_allow_override: Namespace::SR_DISABLED_WITH_OVERRIDE
}
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index c355fa5cc67..93b6b4e8fe2 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -12,35 +12,36 @@ module ClustersHelper
end
end
- def display_cluster_agents?(_clusterable)
- false
+ def display_cluster_agents?(clusterable)
+ clusterable.is_a?(Project)
end
- def js_cluster_agents_list_data(clusterable_project)
- {
- default_branch_name: clusterable_project.default_branch,
- empty_state_image: image_path('illustrations/clusters_empty.svg'),
- project_path: clusterable_project.full_path,
- agent_docs_url: help_page_path('user/clusters/agent/index'),
- install_docs_url: help_page_path('administration/clusters/kas'),
- get_started_docs_url: help_page_path('user/clusters/agent/index', anchor: 'define-a-configuration-repository'),
- integration_docs_url: help_page_path('user/clusters/agent/index', anchor: 'get-started-with-gitops-and-the-gitlab-agent'),
- kas_address: Gitlab::Kas.external_url
- }
- end
-
- def js_clusters_list_data(path = nil)
+ def js_clusters_list_data(clusterable)
{
ancestor_help_path: help_page_path('user/group/clusters/index', anchor: 'cluster-precedence'),
- endpoint: path,
+ endpoint: clusterable.index_path(format: :json),
img_tags: {
aws: { path: image_path('illustrations/logos/amazon_eks.svg'), text: s_('ClusterIntegration|Amazon EKS') },
default: { path: image_path('illustrations/logos/kubernetes.svg'), text: _('Kubernetes Cluster') },
gcp: { path: image_path('illustrations/logos/google_gke.svg'), text: s_('ClusterIntegration|Google GKE') }
- }
+ },
+ clusters_empty_state_image: image_path('illustrations/empty-state/empty-state-clusters.svg'),
+ empty_state_help_text: clusterable.empty_state_help_text,
+ new_cluster_path: clusterable.new_path(tab: 'create'),
+ can_add_cluster: clusterable.can_add_cluster?.to_s
}
end
+ def js_clusters_data(clusterable)
+ {
+ default_branch_name: clusterable.default_branch,
+ empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'),
+ project_path: clusterable.full_path,
+ add_cluster_path: clusterable.new_path(tab: 'add'),
+ kas_address: Gitlab::Kas.external_url
+ }.merge(js_clusters_list_data(clusterable))
+ end
+
def js_cluster_form_data(cluster, can_edit)
{
enabled: cluster.enabled?.to_s,
diff --git a/app/helpers/emoji_helper.rb b/app/helpers/emoji_helper.rb
index 51b7fd7f352..c390924f7e3 100644
--- a/app/helpers/emoji_helper.rb
+++ b/app/helpers/emoji_helper.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
module EmojiHelper
- def emoji_icon(*args)
- raw Gitlab::Emoji.gl_emoji_tag(*args)
+ def emoji_icon(emoji_name, *options)
+ emoji = TanukiEmoji.find_by_alpha_code(emoji_name)
+ raw Gitlab::Emoji.gl_emoji_tag(emoji, *options)
end
end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index b2842664879..cde45e7bc0f 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -69,9 +69,7 @@ module EnvironmentsHelper
'custom_metrics_path' => project_prometheus_metrics_path(project),
'validate_query_path' => validate_query_project_prometheus_metrics_path(project),
'custom_metrics_available' => "#{custom_metrics_available?(project)}",
- 'prometheus_alerts_available' => "#{can?(current_user, :read_prometheus_alerts, project)}",
- 'dashboard_timezone' => project.metrics_setting_dashboard_timezone.to_s.upcase,
- 'has_managed_prometheus' => has_managed_prometheus?(project).to_s
+ 'dashboard_timezone' => project.metrics_setting_dashboard_timezone.to_s.upcase
}
end
@@ -86,10 +84,6 @@ module EnvironmentsHelper
}
end
- def has_managed_prometheus?(project)
- project.prometheus_integration&.prometheus_available? == true
- end
-
def metrics_dashboard_base_path(environment, project)
# This is needed to support our transition from environment scoped metric paths to project scoped.
if project
diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb
index 3a94f7d47c2..45ca820f7b3 100644
--- a/app/helpers/graph_helper.rb
+++ b/app/helpers/graph_helper.rb
@@ -26,6 +26,10 @@ module GraphHelper
def should_render_dora_charts
false
end
+
+ def should_render_quality_summary
+ false
+ end
end
GraphHelper.prepend_mod_with('GraphHelper')
diff --git a/app/helpers/groups/settings_helper.rb b/app/helpers/groups/settings_helper.rb
new file mode 100644
index 00000000000..1b391680996
--- /dev/null
+++ b/app/helpers/groups/settings_helper.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Groups
+ module SettingsHelper
+ include GroupsHelper
+
+ def group_settings_confirm_modal_data(group, remove_form_id = nil)
+ {
+ remove_form_id: remove_form_id,
+ button_text: _('Remove group'),
+ button_testid: 'remove-group-button',
+ disabled: group.paid?.to_s,
+ confirm_danger_message: remove_group_message(group),
+ phrase: group.full_path
+ }
+ end
+ end
+end
+
+Groups::SettingsHelper.prepend_mod_with('Groups::SettingsHelper')
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 30aaa0a5acc..9ba7d004d6c 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -39,7 +39,7 @@ module GroupsHelper
sorted_ancestors(group).with_route.reverse_each.with_index do |parent, index|
if index > 0
- add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before)
+ add_to_breadcrumb_collapsed_links(group_title_link(parent), location: :before)
else
full_title << breadcrumb_list_item(group_title_link(parent, hidable: false))
end
@@ -47,7 +47,7 @@ module GroupsHelper
push_to_schema_breadcrumb(simple_sanitize(parent.name), group_path(parent))
end
- full_title << render("layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups"))
+ full_title << render("layouts/nav/breadcrumbs/collapsed_inline_list", location: :before, title: _("Show all breadcrumbs"))
full_title << breadcrumb_list_item(group_title_link(group))
push_to_schema_breadcrumb(simple_sanitize(group.name), group_path(group))
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index c38b4a7aedf..32d808c960c 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -9,9 +9,7 @@ module IconsHelper
def custom_icon(icon_name, size: DEFAULT_ICON_SIZE)
memoized_icon("#{icon_name}_#{size}") do
- # We can't simply do the below, because there are some .erb SVGs.
- # File.read(Rails.root.join("app/views/shared/icons/_#{icon_name}.svg")).html_safe
- render "shared/icons/#{icon_name}.svg", size: size
+ render partial: "shared/icons/#{icon_name}", formats: :svg, locals: { size: size }
end
end
diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb
index 8819aa9e9cc..bb4a7fef6be 100644
--- a/app/helpers/integrations_helper.rb
+++ b/app/helpers/integrations_helper.rb
@@ -132,6 +132,20 @@ module IntegrationsHelper
end
end
+ def zentao_issue_breadcrumb_link(issue)
+ link_to issue[:web_url], { target: '_blank', rel: 'noopener noreferrer', class: 'gl-display-flex gl-align-items-center gl-white-space-nowrap' } do
+ icon = image_tag image_path('logos/zentao.svg'), width: 15, height: 15, class: 'gl-mr-2'
+ [icon, html_escape(issue[:id])].join.html_safe
+ end
+ end
+
+ def zentao_issues_show_data
+ {
+ issues_show_path: project_integrations_zentao_issue_path(@project, params[:id], format: :json),
+ issues_list_path: project_integrations_zentao_issues_path(@project)
+ }
+ end
+
extend self
private
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index d9bd64f4c2e..01ae0ce4f31 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -42,6 +42,14 @@ module InviteMembersHelper
e.candidate { dataset.merge!(areas_of_focus_options: member_areas_of_focus_options.to_json, no_selection_areas_of_focus: ['no_selection']) }
end
+ if show_invite_members_for_task?
+ dataset.merge!(
+ tasks_to_be_done_options: tasks_to_be_done_options.to_json,
+ projects: projects_for_source(source).to_json,
+ new_project_path: source.is_a?(Group) ? new_project_path(namespace_id: source.id) : ''
+ )
+ end
+
dataset
end
@@ -71,4 +79,19 @@ module InviteMembersHelper
def users_filter_data(group)
{}
end
+
+ def show_invite_members_for_task?
+ return unless current_user && experiment(:invite_members_for_task).enabled?
+
+ params[:open_modal] == 'invite_members_for_task'
+ end
+
+ def tasks_to_be_done_options
+ ::MemberTask::TASKS.keys.map { |task| { value: task, text: localized_tasks_to_be_done_choices[task] } }
+ end
+
+ def projects_for_source(source)
+ projects = source.is_a?(Project) ? [source] : source.projects
+ projects.map { |project| { id: project.id, title: project.title } }
+ end
end
diff --git a/app/helpers/issuables_description_templates_helper.rb b/app/helpers/issuables_description_templates_helper.rb
index a5b9a6eee80..6b546d5c6fc 100644
--- a/app/helpers/issuables_description_templates_helper.rb
+++ b/app/helpers/issuables_description_templates_helper.rb
@@ -32,14 +32,17 @@ module IssuablesDescriptionTemplatesHelper
@template_types[project.id][issuable_type] ||= TemplateFinder.all_template_names(project, issuable_type.pluralize)
end
- # Overriden on EE::IssuablesDescriptionTemplatesHelper to include inherited templates names
- def issuable_templates_names(issuable, include_inherited_templates = false)
+ def selected_template(issuable)
all_templates = issuable_templates(ref_project, issuable.to_ability_name)
- all_templates.values.flatten.map { |tpl| tpl[:name] if tpl[:project_id] == ref_project.id }.compact.uniq
+
+ # Only local templates will be listed if licenses for inherited templates are not present
+ all_templates = all_templates.values.flatten.map { |tpl| tpl[:name] }.compact.uniq
+
+ all_templates.find { |tmpl_name| tmpl_name == params[:issuable_template] }
end
- def selected_template(issuable)
- params[:issuable_template] if issuable_templates_names(issuable, true).any? { |tmpl_name| tmpl_name == params[:issuable_template] }
+ def available_service_desk_templates_for(project)
+ issuable_templates(project, 'issue').flatten.to_json
end
def template_names_path(parent, issuable)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 24c6ef8cd68..07f5adae272 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -257,7 +257,8 @@ module IssuablesHelper
zoomMeetingUrl: ZoomMeeting.canonical_meeting_url(issuable),
sentryIssueIdentifier: SentryIssue.find_by(issue: issuable)&.sentry_issue_identifier, # rubocop:disable CodeReuse/ActiveRecord
iid: issuable.iid.to_s,
- isHidden: issue_hidden?(issuable)
+ isHidden: issue_hidden?(issuable),
+ canCreateIncident: create_issue_type_allowed?(issuable.project, :incident)
}
end
@@ -284,9 +285,7 @@ module IssuablesHelper
end
def issuables_count_for_state(issuable_type, state)
- store_in_cache = parent.is_a?(Group) ? parent.cached_issues_state_count_enabled? : false
-
- Gitlab::IssuablesCountForState.new(finder, store_in_redis_cache: store_in_cache)[state]
+ Gitlab::IssuablesCountForState.new(finder, store_in_redis_cache: true)[state]
end
def close_issuable_path(issuable)
@@ -442,7 +441,7 @@ module IssuablesHelper
end
def format_count(issuable_type, count, threshold)
- if issuable_type == :issues && parent.is_a?(Group) && parent.cached_issues_state_count_enabled?
+ if issuable_type == :issues && parent.is_a?(Group)
format_cached_count(threshold, count)
else
number_with_delimiter(count)
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 49f7d9aeef1..a88ca6f6b11 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module IssuesHelper
+ include Issues::IssueTypeHelpers
+
def issue_css_classes(issue)
classes = ["issue"]
classes << "closed" if issue.closed?
@@ -190,6 +192,7 @@ module IssuesHelper
{
can_create_issue: show_new_issue_link?(project).to_s,
+ can_create_incident: create_issue_type_allowed?(project, :incident).to_s,
can_reopen_issue: can?(current_user, :reopen_issue, issuable).to_s,
can_report_spam: issuable.submittable_as_spam_by?(current_user).to_s,
can_update_issue: can?(current_user, :update_issue, issuable).to_s,
@@ -233,6 +236,7 @@ module IssuesHelper
new_issue_path: new_project_issue_path(project, issue: { milestone_id: finder.milestones.first.try(:id) }),
project_import_jira_path: project_import_jira_path(project),
quick_actions_help_path: help_page_path('user/project/quick_actions'),
+ releases_path: project_releases_path(project, format: :json),
reset_path: new_issuable_address_project_path(project, issuable_type: 'issue'),
show_new_issue_link: show_new_issue_link?(project).to_s
)
diff --git a/app/helpers/learn_gitlab_helper.rb b/app/helpers/learn_gitlab_helper.rb
index 4fb7a05a0e9..08a30c4d53b 100644
--- a/app/helpers/learn_gitlab_helper.rb
+++ b/app/helpers/learn_gitlab_helper.rb
@@ -7,10 +7,32 @@ module LearnGitlabHelper
learn_gitlab_onboarding_available?(project)
end
+ def learn_gitlab_data(project)
+ {
+ actions: onboarding_actions_data(project).to_json,
+ sections: onboarding_sections_data.to_json
+ }
+ end
+
+ def learn_gitlab_onboarding_available?(project)
+ OnboardingProgress.onboarding?(project.namespace) &&
+ LearnGitlab::Project.new(current_user).available?
+ end
+
+ private
+
def onboarding_actions_data(project)
attributes = onboarding_progress(project).attributes.symbolize_keys
- action_urls.to_h do |action, url|
+ urls_to_use = nil
+
+ experiment(:change_continuous_onboarding_link_urls) do |e|
+ e.namespace = project.namespace
+ e.use { urls_to_use = action_urls }
+ e.try { urls_to_use = new_action_urls(project) }
+ end
+
+ urls_to_use.to_h do |action, url|
[
action,
url: url,
@@ -34,18 +56,22 @@ module LearnGitlabHelper
}
end
- def learn_gitlab_onboarding_available?(project)
- OnboardingProgress.onboarding?(project.namespace) &&
- LearnGitlab::Project.new(current_user).available?
- end
-
- private
-
def action_urls
LearnGitlab::Onboarding::ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }
.merge(LearnGitlab::Onboarding::ACTION_DOC_URLS)
end
+ def new_action_urls(project)
+ action_urls.merge(
+ issue_created: project_issues_path(project),
+ git_write: project_path(project),
+ pipeline_created: project_pipelines_path(project),
+ merge_request_created: project_merge_requests_path(project),
+ user_added: project_members_url(project),
+ security_scan_enabled: project_security_configuration_path(project)
+ )
+ end
+
def learn_gitlab_project
@learn_gitlab_project ||= LearnGitlab::Project.new(current_user).project
end
@@ -54,3 +80,5 @@ module LearnGitlabHelper
OnboardingProgress.find_by(namespace: project.namespace) # rubocop: disable CodeReuse/ActiveRecord
end
end
+
+LearnGitlabHelper.prepend_mod_with('LearnGitlabHelper')
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index d3db5d24207..aac49cfa234 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -56,6 +56,14 @@ module MembersHelper
end
end
+ def localized_tasks_to_be_done_choices
+ {
+ code: s_('TasksToBeDone|Create/import code into a project (repository)'),
+ ci: s_('TasksToBeDone|Set up CI/CD pipelines to build, test, deploy, and monitor code'),
+ issues: s_('TasksToBeDone|Create/import issues (tickets) to collaborate on ideas and plan work')
+ }.freeze
+ end
+
private
def source_text(member)
diff --git a/app/helpers/nav/top_nav_helper.rb b/app/helpers/nav/top_nav_helper.rb
index 3055ad57b80..ecef2d38e54 100644
--- a/app/helpers/nav/top_nav_helper.rb
+++ b/app/helpers/nav/top_nav_helper.rb
@@ -267,6 +267,7 @@ module Nav
builder.add_primary_menu_item(id: 'your', title: _('Your projects'), href: dashboard_projects_path)
builder.add_primary_menu_item(id: 'starred', title: _('Starred projects'), href: starred_dashboard_projects_path)
builder.add_primary_menu_item(id: 'explore', title: _('Explore projects'), href: explore_root_path)
+ builder.add_primary_menu_item(id: 'topics', title: _('Explore topics'), href: topics_explore_projects_path)
builder.add_secondary_menu_item(id: 'create', title: _('Create new project'), href: new_project_path)
builder.build
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index fff7e5d1c7f..2dadaa0be0a 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -167,11 +167,11 @@ module NotesHelper
}
end
- def discussions_path(issuable)
+ def discussions_path(issuable, **params)
if issuable.is_a?(Issue)
- discussions_project_issue_path(@project, issuable, format: :json)
+ discussions_project_issue_path(@project, issuable, params.merge(format: :json))
else
- discussions_project_merge_request_path(@project, issuable, format: :json)
+ discussions_project_merge_request_path(@project, issuable, params.merge(format: :json))
end
end
@@ -188,7 +188,8 @@ module NotesHelper
reopenPath: reopen_issuable_path(issuable),
notesPath: notes_url,
prerenderedNotesCount: issuable.capped_notes_count(MAX_PRERENDERED_NOTES),
- lastFetchedAt: initial_last_fetched_at
+ lastFetchedAt: initial_last_fetched_at,
+ notesFilter: current_user&.notes_filter_for(issuable)
}
if issuable.is_a?(MergeRequest)
diff --git a/app/helpers/one_trust_helper.rb b/app/helpers/one_trust_helper.rb
index 9f92a73a4d4..55364d36a0e 100644
--- a/app/helpers/one_trust_helper.rb
+++ b/app/helpers/one_trust_helper.rb
@@ -4,7 +4,6 @@ module OneTrustHelper
def one_trust_enabled?
Feature.enabled?(:ecomm_instrumentation, type: :ops) &&
Gitlab.config.extra.has_key?('one_trust_id') &&
- Gitlab.config.extra.one_trust_id.present? &&
- !current_user
+ Gitlab.config.extra.one_trust_id.present?
end
end
diff --git a/app/helpers/projects/alert_management_helper.rb b/app/helpers/projects/alert_management_helper.rb
index b50e287a509..e03f2ae78bf 100644
--- a/app/helpers/projects/alert_management_helper.rb
+++ b/app/helpers/projects/alert_management_helper.rb
@@ -10,7 +10,6 @@ module Projects::AlertManagementHelper
'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg'),
'user-can-enable-alert-management' => can?(current_user, :admin_operations, project).to_s,
'alert-management-enabled' => alert_management_enabled?(project).to_s,
- 'has-managed-prometheus' => has_managed_prometheus?(project).to_s,
'text-query': params[:search],
'assignee-username-query': params[:assignee_username]
}
@@ -28,10 +27,6 @@ module Projects::AlertManagementHelper
private
- def has_managed_prometheus?(project)
- project.prometheus_integration&.prometheus_available? == true
- end
-
def alert_management_enabled?(project)
!!(
project.alert_management_alerts.any? ||
diff --git a/app/helpers/projects/incidents_helper.rb b/app/helpers/projects/incidents_helper.rb
index dde2980817f..4b3ff8a3e83 100644
--- a/app/helpers/projects/incidents_helper.rb
+++ b/app/helpers/projects/incidents_helper.rb
@@ -11,7 +11,8 @@ module Projects::IncidentsHelper
'empty-list-svg-path' => image_path('illustrations/incident-empty-state.svg'),
'text-query': params[:search],
'author-username-query': params[:author_username],
- 'assignee-username-query': params[:assignee_username]
+ 'assignee-username-query': params[:assignee_username],
+ 'can-create-incident': create_issue_type_allowed?(project, :incident).to_s
}
end
end
diff --git a/app/helpers/projects/security/configuration_helper.rb b/app/helpers/projects/security/configuration_helper.rb
index dee106ab3ae..8281b1f8522 100644
--- a/app/helpers/projects/security/configuration_helper.rb
+++ b/app/helpers/projects/security/configuration_helper.rb
@@ -4,7 +4,7 @@ module Projects
module Security
module ConfigurationHelper
def security_upgrade_path
- 'https://about.gitlab.com/pricing/'
+ "https://#{ApplicationHelper.promo_host}/pricing/"
end
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index e3b63d122d2..8366b25d2bc 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -196,12 +196,26 @@ module ProjectsHelper
cookies["hide_auto_devops_implicitly_enabled_banner_#{project.id}".to_sym].blank?
end
- def link_to_set_password
- if current_user.require_password_creation_for_git?
- link_to s_('SetPasswordToCloneLink|set a password'), edit_profile_password_path
- else
- link_to s_('CreateTokenToCloneLink|create a personal access token'), profile_personal_access_tokens_path
- end
+ def no_password_message
+ push_pull_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('gitlab-basics/start-using-git', anchor: 'pull-and-push') }
+ clone_with_https_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('gitlab-basics/start-using-git', anchor: 'clone-with-https') }
+ set_password_link_start = '<a href="%{url}">'.html_safe % { url: edit_profile_password_path }
+ set_up_pat_link_start = '<a href="%{url}">'.html_safe % { url: profile_personal_access_tokens_path }
+
+ message = if current_user.require_password_creation_for_git?
+ _('Your account is authenticated with SSO or SAML. To %{push_pull_link_start}push and pull%{link_end} over %{protocol} with Git using this account, you must %{set_password_link_start}set a password%{link_end} or %{set_up_pat_link_start}set up a Personal Access Token%{link_end} to use instead of a password. For more information, see %{clone_with_https_link_start}Clone with HTTPS%{link_end}.')
+ else
+ _('Your account is authenticated with SSO or SAML. To %{push_pull_link_start}push and pull%{link_end} over %{protocol} with Git using this account, you must %{set_up_pat_link_start}set up a Personal Access Token%{link_end} to use instead of a password. For more information, see %{clone_with_https_link_start}Clone with HTTPS%{link_end}.')
+ end
+
+ html_escape(message) % {
+ push_pull_link_start: push_pull_link_start,
+ protocol: gitlab_config.protocol.upcase,
+ clone_with_https_link_start: clone_with_https_link_start,
+ set_password_link_start: set_password_link_start,
+ set_up_pat_link_start: set_up_pat_link_start,
+ link_end: '</a>'.html_safe
+ }
end
# Returns true if any projects are present.
@@ -382,6 +396,15 @@ module ProjectsHelper
""
end
+ # Returns the confirm phrase the user needs to type in order to delete the project
+ #
+ # Thus the phrase should include the namespace to make it very clear to the
+ # user which project is subject to deletion.
+ # Relevant issue: https://gitlab.com/gitlab-org/gitlab/-/issues/343591
+ def delete_confirm_phrase(project)
+ project.path_with_namespace
+ end
+
private
def tab_ability_map
@@ -590,6 +613,7 @@ module ProjectsHelper
%w[
environments
clusters
+ cluster_agents
functions
error_tracking
alert_management
diff --git a/app/helpers/recaptcha_helper.rb b/app/helpers/recaptcha_helper.rb
index 0df62f7b715..5b17ab4b815 100644
--- a/app/helpers/recaptcha_helper.rb
+++ b/app/helpers/recaptcha_helper.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
module RecaptchaHelper
- def show_recaptcha_sign_up?
+ def recaptcha_enabled?
!!Gitlab::Recaptcha.enabled?
end
+ alias_method :show_recaptcha_sign_up?, :recaptcha_enabled?
end
RecaptchaHelper.prepend_mod
diff --git a/app/helpers/reminder_emails_helper.rb b/app/helpers/reminder_emails_helper.rb
index bffb3cf7751..132fc3b784c 100644
--- a/app/helpers/reminder_emails_helper.rb
+++ b/app/helpers/reminder_emails_helper.rb
@@ -7,7 +7,8 @@ module ReminderEmailsHelper
s_('InviteReminderEmail|Invitation pending')
when 1
if format == :html
- s_('InviteReminderEmail|Hey there %{wave_emoji}').html_safe % { wave_emoji: Gitlab::Emoji.gl_emoji_tag('wave') }
+ wave_emoji_tag = Gitlab::Emoji.gl_emoji_tag(TanukiEmoji.find_by_alpha_code('wave'))
+ s_('InviteReminderEmail|Hey there %{wave_emoji}').html_safe % { wave_emoji: wave_emoji_tag }
else
s_('InviteReminderEmail|Hey there!')
end
diff --git a/app/helpers/routing/pseudonymization_helper.rb b/app/helpers/routing/pseudonymization_helper.rb
index b73e49803ae..ac30669dc83 100644
--- a/app/helpers/routing/pseudonymization_helper.rb
+++ b/app/helpers/routing/pseudonymization_helper.rb
@@ -2,58 +2,86 @@
module Routing
module PseudonymizationHelper
- def masked_page_url
- return unless Feature.enabled?(:mask_page_urls, type: :ops)
+ class MaskHelper
+ QUERY_PARAMS_TO_NOT_MASK = %w[].freeze
- mask_params(Rails.application.routes.recognize_path(request.original_fullpath))
- rescue ActionController::RoutingError, URI::InvalidURIError => e
- Gitlab::ErrorTracking.track_exception(e, url: request.original_fullpath)
- nil
- end
+ def initialize(request_object, group, project)
+ @request = request_object
+ @group = group
+ @project = project
+ end
+
+ def mask_params
+ return default_root_url + @request.original_fullpath unless has_maskable_params?
- private
+ masked_params = @request.path_parameters.to_h do |key, value|
+ case key
+ when :project_id
+ [key, "project#{@project&.id}"]
+ when :namespace_id, :group_id
+ namespace = @group || @project&.namespace
+ [key, "namespace#{namespace&.id}"]
+ when :id
+ [key, mask_id(value)]
+ else
+ [key, value]
+ end
+ end
- def mask_params(request_params)
- return if request_params[:action] == 'new'
+ Gitlab::Routing.url_helpers.url_for(masked_params.merge(params: masked_query_params))
+ end
- namespace_type = request_params[:controller].split('/')[1]
+ private
- namespace_type.present? ? url_with_namespace_type(request_params, namespace_type) : url_without_namespace_type(request_params)
- end
+ def mask_id(value)
+ if @request.path_parameters[:controller] == 'projects/blob'
+ ':repository_path'
+ elsif @request.path_parameters[:controller] == 'projects'
+ "project#{@project&.id}"
+ elsif @request.path_parameters[:controller] == 'groups'
+ "namespace#{@group&.id}"
+ else
+ value
+ end
+ end
- def url_without_namespace_type(request_params)
- masked_url = "#{request.protocol}#{request.host_with_port}"
+ def has_maskable_params?
+ request_params = @request.path_parameters.to_h
+ request_params.key?(:namespace_id) || request_params.key?(:group_id) || request_params.key?(:project_id) || request_params.key?(:id) || @request.query_string.present?
+ end
- masked_url += case request_params[:controller]
- when 'groups'
- "/namespace:#{group.id}"
- when 'projects'
- "/namespace:#{project.namespace_id}/project:#{project.id}"
- when 'root'
- ''
- else
- "#{request.path}"
- end
+ def masked_query_params
+ return {} unless @request.query_string.present?
- masked_url += request.query_string.present? ? "?#{request.query_string}" : ''
+ query_string_hash = Rack::Utils.parse_nested_query(@request.query_string)
- masked_url
- end
+ query_string_hash.keys.each do |key|
+ next if QUERY_PARAMS_TO_NOT_MASK.include?(key)
- def url_with_namespace_type(request_params, namespace_type)
- masked_url = "#{request.protocol}#{request.host_with_port}"
+ query_string_hash[key] = "masked_#{key}"
+ end
- if request_params.has_key?(:project_id)
- masked_url += "/namespace:#{project.namespace_id}/project:#{project.id}/-/#{namespace_type}"
+ query_string_hash
end
- if request_params.has_key?(:id)
- masked_url += namespace_type == 'blob' ? '/:repository_path' : "/#{request_params[:id]}"
+ def default_root_url
+ Gitlab::Routing.url_helpers.root_url(only_path: false)
end
+ end
- masked_url += request.query_string.present? ? "?#{request.query_string}" : ''
+ def masked_page_url
+ return unless Feature.enabled?(:mask_page_urls, type: :ops)
+
+ current_group = group if defined?(group)
+ current_project = project if defined?(project)
+ mask_helper = MaskHelper.new(request, current_group, current_project)
+ mask_helper.mask_params
- masked_url
+ # We rescue all exception for time being till we test this helper extensively.
+ # Check https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72864#note_711515501
+ rescue => e # rubocop:disable Style/RescueStandardError
+ Gitlab::ErrorTracking.track_exception(e, url: request.original_fullpath)
+ nil
end
end
end
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
index d6a4d6ac57a..a60143db739 100644
--- a/app/helpers/storage_helper.rb
+++ b/app/helpers/storage_helper.rb
@@ -14,12 +14,13 @@ module StorageHelper
counter_repositories: storage_counter(statistics.repository_size),
counter_wikis: storage_counter(statistics.wiki_size),
counter_build_artifacts: storage_counter(statistics.build_artifacts_size),
+ counter_pipeline_artifacts: storage_counter(statistics.pipeline_artifacts_size),
counter_lfs_objects: storage_counter(statistics.lfs_objects_size),
counter_snippets: storage_counter(statistics.snippets_size),
counter_packages: storage_counter(statistics.packages_size),
counter_uploads: storage_counter(statistics.uploads_size)
}
- _("Repository: %{counter_repositories} / Wikis: %{counter_wikis} / Build Artifacts: %{counter_build_artifacts} / LFS: %{counter_lfs_objects} / Snippets: %{counter_snippets} / Packages: %{counter_packages} / Uploads: %{counter_uploads}") % counters
+ _("Repository: %{counter_repositories} / Wikis: %{counter_wikis} / Build Artifacts: %{counter_build_artifacts} / Pipeline Artifacts: %{counter_pipeline_artifacts} / LFS: %{counter_lfs_objects} / Snippets: %{counter_snippets} / Packages: %{counter_packages} / Uploads: %{counter_uploads}") % counters
end
end
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index a6bb2f3b246..e53e35baac3 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -33,7 +33,6 @@ module TabHelper
# :item_active - Overrides the default state focing the "active" css classes (optional).
#
def gl_tab_link_to(name = nil, options = {}, html_options = {}, &block)
- tab_class = 'nav-item'
link_classes = %w[nav-link gl-tab-nav-item]
active_link_classes = %w[active gl-tab-nav-item-active gl-tab-nav-item-active-indigo]
@@ -52,6 +51,8 @@ module TabHelper
end
html_options = html_options.except(:item_active)
+ extra_tab_classes = html_options.delete(:tab_class)
+ tab_class = %w[nav-item].push(*extra_tab_classes)
content_tag(:li, class: tab_class, role: 'presentation') do
if block_given?
@@ -210,3 +211,12 @@ module TabHelper
current_page?(options)
end
end
+
+def gl_tab_counter_badge(count, html_options = {})
+ badge_classes = %w[badge badge-muted badge-pill gl-badge sm gl-tab-counter-badge]
+ content_tag(:span,
+ count,
+ class: [*html_options[:class], badge_classes].join(' '),
+ data: html_options[:data]
+ )
+end
diff --git a/app/helpers/terms_helper.rb b/app/helpers/terms_helper.rb
new file mode 100644
index 00000000000..5f321551413
--- /dev/null
+++ b/app/helpers/terms_helper.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module TermsHelper
+ def terms_data(terms, redirect)
+ redirect_params = { redirect: redirect } if redirect
+
+ {
+ terms: markdown_field(terms, :terms),
+ permissions: {
+ can_accept: can?(current_user, :accept_terms, terms),
+ can_decline: can?(current_user, :decline_terms, terms)
+ },
+ paths: {
+ accept: accept_term_path(terms, redirect_params),
+ decline: decline_term_path(terms, redirect_params),
+ root: root_path
+ }
+ }.to_json
+ end
+end
diff --git a/app/helpers/time_zone_helper.rb b/app/helpers/time_zone_helper.rb
index a0d9c8403e8..db355f5ff65 100644
--- a/app/helpers/time_zone_helper.rb
+++ b/app/helpers/time_zone_helper.rb
@@ -32,10 +32,16 @@ module TimeZoneHelper
end
end
+ def local_time_instance(timezone)
+ return Time.zone if timezone.blank?
+
+ ActiveSupport::TimeZone.new(timezone) || Time.zone
+ end
+
def local_time(timezone)
return if timezone.blank?
- time_zone_instance = ActiveSupport::TimeZone.new(timezone) || Time.zone
+ time_zone_instance = local_time_instance(timezone)
time_zone_instance.now.strftime("%-l:%M %p")
end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index e9dc271dbdd..79767ca76b7 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -24,6 +24,7 @@ module TodosHelper
when Todo::UNMERGEABLE then 'Could not merge'
when Todo::DIRECTLY_ADDRESSED then "directly addressed #{todo_action_subject(todo)} on"
when Todo::MERGE_TRAIN_REMOVED then "Removed from Merge Train:"
+ when Todo::ATTENTION_REQUESTED then 'requested your attention on'
end
end
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 1c67ca983fa..d8e69145c40 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -81,32 +81,11 @@ module UserCalloutsHelper
def user_dismissed_for_group(feature_name, group, ignore_dismissal_earlier_than = nil)
return false unless current_user
- set_dismissed_from_cookie(group)
-
current_user.dismissed_callout_for_group?(feature_name: feature_name,
group: group,
ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
end
- def set_dismissed_from_cookie(group)
- # bridge function for one milestone to try and not annoy users who might have already dismissed this alert
- # remove in 14.4 or 14.5? https://gitlab.com/gitlab-org/gitlab/-/issues/340322
- dismissed_key = "invite_#{group.id}_#{current_user.id}"
-
- if cookies[dismissed_key].present?
- params = {
- feature_name: INVITE_MEMBERS_BANNER,
- group_id: group.id
- }
-
- Users::DismissGroupCalloutService.new(
- container: nil, current_user: current_user, params: params
- ).execute
-
- cookies.delete dismissed_key
- end
- end
-
def just_created?
flash[:notice]&.include?('successfully created')
end
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index 1b0d1254dc8..ba876f6cb65 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -9,7 +9,7 @@ module WikiHelper
if page.persisted?
titles << page.human_title
breadcrumb_title(page.human_title)
- wiki_breadcrumb_dropdown_links(page.slug)
+ wiki_breadcrumb_collapsed_links(page.slug)
end
titles << action if action
@@ -39,14 +39,14 @@ module WikiHelper
.join(' / ')
end
- def wiki_breadcrumb_dropdown_links(page_slug)
+ def wiki_breadcrumb_collapsed_links(page_slug)
page_slug_split = page_slug.split('/')
page_slug_split.pop(1)
current_slug = ""
page_slug_split
.map do |dir_or_page|
current_slug = "#{current_slug}#{dir_or_page}/"
- add_to_breadcrumb_dropdown link_to(WikiPage.unhyphenize(dir_or_page).capitalize, wiki_page_path(@wiki, current_slug)), location: :after
+ add_to_breadcrumb_collapsed_links link_to(WikiPage.unhyphenize(dir_or_page).capitalize, wiki_page_path(@wiki, current_slug)), location: :after
end
end
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index 4862282bc73..2460c956bb6 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -41,8 +41,8 @@ module WorkhorseHelper
head :ok
end
- def send_dependency(token, url, filename)
- headers.store(*Gitlab::Workhorse.send_dependency(token, url))
+ def send_dependency(dependency_headers, url, filename)
+ headers.store(*Gitlab::Workhorse.send_dependency(dependency_headers, url))
headers['Content-Disposition'] =
ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: filename)
headers['Content-Type'] = 'application/gzip'
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 6954fd46850..8a9ed557cc6 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -43,9 +43,13 @@ module Emails
user = User.find(user_id)
+ @source_hidden = !member_source.readable_by?(user)
+
+ human_name = @source_hidden ? 'Hidden' : member_source.human_name
+
member_email_with_layout(
to: user.notification_email_for(notification_group),
- subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was denied"))
+ subject: subject("Access to the #{human_name} #{member_source.model_name.singular} was denied"))
end
def member_invited_email(member_source_type, member_id, token)
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index 1b27d062391..5363ad63771 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -2,21 +2,23 @@
module Emails
module Pipelines
- def pipeline_success_email(pipeline, recipients)
- pipeline_mail(pipeline, recipients, 'Successful')
+ def pipeline_success_email(pipeline, recipient)
+ pipeline_mail(pipeline, recipient, 'Successful')
end
- def pipeline_failed_email(pipeline, recipients)
- pipeline_mail(pipeline, recipients, 'Failed')
+ def pipeline_failed_email(pipeline, recipient)
+ pipeline_mail(pipeline, recipient, 'Failed')
end
- def pipeline_fixed_email(pipeline, recipients)
- pipeline_mail(pipeline, recipients, 'Fixed')
+ def pipeline_fixed_email(pipeline, recipient)
+ pipeline_mail(pipeline, recipient, 'Fixed')
end
private
- def pipeline_mail(pipeline, recipients, status)
+ def pipeline_mail(pipeline, recipient, status)
+ raise ArgumentError if recipient.is_a?(Array)
+
@project = pipeline.project
@pipeline = pipeline
@@ -28,10 +30,7 @@ module Emails
add_headers
- # We use bcc here because we don't want to generate these emails for a
- # thousand times. This could be potentially expensive in a loop, and
- # recipients would contain all project watchers so it could be a lot.
- mail(bcc: recipients,
+ mail(to: recipient,
subject: subject(pipeline_subject(status))) do |format|
format.html { render layout: 'mailer' }
format.text { render layout: 'mailer' }
diff --git a/app/models/analytics/cycle_analytics/issue_stage_event.rb b/app/models/analytics/cycle_analytics/issue_stage_event.rb
index 3e6ed86d534..837eb35c839 100644
--- a/app/models/analytics/cycle_analytics/issue_stage_event.rb
+++ b/app/models/analytics/cycle_analytics/issue_stage_event.rb
@@ -8,9 +8,22 @@ module Analytics
validates(*%i[stage_event_hash_id issue_id group_id project_id start_event_timestamp], presence: true)
+ alias_attribute :state, :state_id
+ enum state: Issue.available_states, _suffix: true
+
+ scope :assigned_to, ->(user) do
+ assignees_class = IssueAssignee
+ condition = assignees_class.where(user_id: user).where(arel_table[:issue_id].eq(assignees_class.arel_table[:issue_id]))
+ where(condition.arel.exists)
+ end
+
def self.issuable_id_column
:issue_id
end
+
+ def self.issuable_model
+ ::Issue
+ end
end
end
end
diff --git a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
index d0ec3c4e8b9..0dfa322b2c3 100644
--- a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
+++ b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
@@ -8,9 +8,22 @@ module Analytics
validates(*%i[stage_event_hash_id merge_request_id group_id project_id start_event_timestamp], presence: true)
+ alias_attribute :state, :state_id
+ enum state: MergeRequest.available_states, _suffix: true
+
+ scope :assigned_to, ->(user) do
+ assignees_class = MergeRequestAssignee
+ condition = assignees_class.where(user_id: user).where(arel_table[:merge_request_id].eq(assignees_class.arel_table[:merge_request_id]))
+ where(condition.arel.exists)
+ end
+
def self.issuable_id_column
:merge_request_id
end
+
+ def self.issuable_model
+ ::MergeRequest
+ end
end
end
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index d2757d8c17d..bcd8bdd6638 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -1,7 +1,10 @@
# frozen_string_literal: true
class ApplicationRecord < ActiveRecord::Base
- self.gitlab_schema = :gitlab_main
+ include DatabaseReflection
+ include Transactions
+ include LegacyBulkInsert
+
self.abstract_class = true
alias_method :reset, :reload
@@ -92,8 +95,7 @@ class ApplicationRecord < ActiveRecord::Base
end
def self.declarative_enum(enum_mod)
- values = enum_mod.definition.transform_values { |v| v[:value] }
- enum(enum_mod.key => values)
+ enum(enum_mod.key => enum_mod.values)
end
def self.cached_column_list
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 5a8cbd8d71c..af5796d682f 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -536,6 +536,18 @@ class ApplicationSetting < ApplicationRecord
validates :sidekiq_job_limiter_limit_bytes,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :sentry_enabled,
+ inclusion: { in: [true, false], message: _('must be a boolean value') }
+ validates :sentry_dsn,
+ addressable_url: true, presence: true, length: { maximum: 255 },
+ if: :sentry_enabled?
+ validates :sentry_clientside_dsn,
+ addressable_url: true, allow_blank: true, length: { maximum: 255 },
+ if: :sentry_enabled?
+ validates :sentry_environment,
+ presence: true, length: { maximum: 255 },
+ if: :sentry_enabled?
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 7bdea36bb8a..54ec8b2c3e4 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -146,6 +146,9 @@ module ApplicationSettingImplementation
session_expire_delay: Settings.gitlab['session_expire_delay'],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
shared_runners_text: nil,
+ sidekiq_job_limiter_mode: Gitlab::SidekiqMiddleware::SizeLimiter::Validator::COMPRESS_MODE,
+ sidekiq_job_limiter_compression_threshold_bytes: Gitlab::SidekiqMiddleware::SizeLimiter::Validator::DEFAULT_COMPRESSION_THRESHOLD_BYTES,
+ sidekiq_job_limiter_limit_bytes: Gitlab::SidekiqMiddleware::SizeLimiter::Validator::DEFAULT_SIZE_LIMIT,
sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'],
snippet_size_limit: 50.megabytes,
@@ -241,11 +244,11 @@ module ApplicationSettingImplementation
end
def home_page_url_column_exists?
- ::Gitlab::Database.main.cached_column_exists?(:application_settings, :home_page_url)
+ ApplicationSetting.database.cached_column_exists?(:home_page_url)
end
def help_page_support_url_column_exists?
- ::Gitlab::Database.main.cached_column_exists?(:application_settings, :help_page_support_url)
+ ApplicationSetting.database.cached_column_exists?(:help_page_support_url)
end
def disabled_oauth_sign_in_sources=(sources)
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index a1c6793607f..1a8bd05c42c 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -30,6 +30,8 @@ class AuditEvent < ApplicationRecord
scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) }
scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) }
scope :by_author_id, -> (author_id) { where(author_id: author_id) }
+ scope :by_entity_username, -> (username) { where(entity_id: find_user_id(username)) }
+ scope :by_author_username, -> (username) { where(author_id: find_user_id(username)) }
after_initialize :initialize_details
@@ -106,6 +108,10 @@ class AuditEvent < ApplicationRecord
self[name] = self.details[name] = original
end
end
+
+ def self.find_user_id(username)
+ User.find_by_username(username)&.id
+ end
end
AuditEvent.prepend_mod_with('AuditEvent')
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index c8f6b9aaedb..b665f3d5d8c 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -14,7 +14,7 @@ class AwardEmoji < ApplicationRecord
validates :user, presence: true
validates :awardable, presence: true, unless: :importing?
- validates :name, presence: true, inclusion: { in: Gitlab::Emoji.emojis_names }
+ validates :name, presence: true, 'gitlab/emoji_name': true
validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] }, unless: -> { ghost_user? || importing? }
participant :user
diff --git a/app/models/blob_viewer/package_json.rb b/app/models/blob_viewer/package_json.rb
index 7cae60a74d6..1d10cc82a85 100644
--- a/app/models/blob_viewer/package_json.rb
+++ b/app/models/blob_viewer/package_json.rb
@@ -7,11 +7,15 @@ module BlobViewer
self.file_types = %i(package_json)
def manager_name
- 'npm'
+ yarn? ? 'yarn' : 'npm'
+ end
+
+ def yarn?
+ json_data['engines'].present? && json_data['engines']['yarn'].present?
end
def manager_url
- 'https://www.npmjs.com/'
+ yarn? ? 'https://yarnpkg.com/' : 'https://www.npmjs.com/'
end
def package_name
@@ -38,7 +42,11 @@ module BlobViewer
end
def npm_url
- "https://www.npmjs.com/package/#{package_name}"
+ if yarn?
+ "https://yarnpkg.com/package/#{package_name}"
+ else
+ "https://www.npmjs.com/package/#{package_name}"
+ end
end
end
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index ecac4ab95f4..2368be6196c 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -112,6 +112,10 @@ class BulkImports::Entity < ApplicationRecord
@export_relations_url_path ||= EXPORT_RELATIONS_URL % { resource: pluralized_name, full_path: encoded_source_full_path }
end
+ def relation_download_url_path(relation)
+ "#{export_relations_url_path}/download?relation=#{relation}"
+ end
+
private
def validate_parent_is_a_group
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
index ff3f2663b73..da7312df18b 100644
--- a/app/models/chat_name.rb
+++ b/app/models/chat_name.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ChatName < ApplicationRecord
+ include LooseForeignKey
+
LAST_USED_AT_INTERVAL = 1.hour
belongs_to :integration, foreign_key: :service_id
@@ -14,6 +16,8 @@ class ChatName < ApplicationRecord
validates :user_id, uniqueness: { scope: [:service_id] }
validates :chat_id, uniqueness: { scope: [:service_id, :team_id] }
+ loose_foreign_key :ci_pipeline_chat_data, :chat_name_id, on_delete: :async_delete
+
# Updates the "last_used_timestamp" but only if it wasn't already updated
# recently.
#
diff --git a/app/models/ci/application_record.rb b/app/models/ci/application_record.rb
index 913e7a62c66..ea7b1104e36 100644
--- a/app/models/ci/application_record.rb
+++ b/app/models/ci/application_record.rb
@@ -2,9 +2,12 @@
module Ci
class ApplicationRecord < ::ApplicationRecord
- self.gitlab_schema = :gitlab_ci
self.abstract_class = true
+ if Gitlab::Database.has_config?(:ci)
+ connects_to database: { writing: :ci, reading: :ci }
+ end
+
def self.table_name_prefix
'ci_'
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 990ef71a457..3fdc44bccf3 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -10,7 +10,6 @@ module Ci
include Presentable
include Importable
include Ci::HasRef
- include IgnorableColumns
BuildArchivedError = Class.new(StandardError)
@@ -70,9 +69,6 @@ module Ci
delegate :gitlab_deploy_token, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
- ignore_columns :id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
- ignore_columns :stage_id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
-
##
# Since Gitlab 11.5, deployments records started being created right after
# `ci_builds` creation. We can look up a relevant `environment` through
@@ -175,6 +171,7 @@ module Ci
scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) }
scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) }
scope :finished_before, -> (date) { finished.where('finished_at < ?', date) }
+ scope :license_management_jobs, -> { where(name: %i(license_management license_scanning)) } # handle license rename https://gitlab.com/gitlab-org/gitlab/issues/8911
scope :with_secure_reports_from_config_options, -> (job_types) do
joins(:metadata).where("ci_builds_metadata.config_options -> 'artifacts' -> 'reports' ?| array[:job_types]", job_types: job_types)
@@ -313,12 +310,6 @@ module Ci
end
after_transition pending: :running do |build|
- unless build.update_deployment_after_transaction_commit?
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
- build.deployment&.run
- end
- end
-
build.run_after_commit do
build.pipeline.persistent_ref.create
@@ -339,12 +330,6 @@ module Ci
end
after_transition any => [:success] do |build|
- unless build.update_deployment_after_transaction_commit?
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
- build.deployment&.succeed
- end
- end
-
build.run_after_commit do
BuildSuccessWorker.perform_async(id)
PagesWorker.perform_async(:deploy, id) if build.pages_generator?
@@ -353,23 +338,6 @@ module Ci
after_transition any => [:failed] do |build|
next unless build.project
- next unless build.deployment
-
- unless build.update_deployment_after_transaction_commit?
- begin
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
- build.deployment.drop!
- end
- rescue StandardError => e
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, build_id: build.id)
- end
- end
-
- true
- end
-
- after_transition any => [:failed] do |build|
- next unless build.project
if build.auto_retry_allowed?
begin
@@ -380,25 +348,12 @@ module Ci
end
end
- after_transition any => [:skipped, :canceled] do |build, transition|
- unless build.update_deployment_after_transaction_commit?
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
- if transition.to_name == :skipped
- build.deployment&.skip
- else
- build.deployment&.cancel
- end
- end
- end
- end
-
# Synchronize Deployment Status
# Please note that the data integirty is not assured because we can't use
# a database transaction due to DB decomposition.
after_transition do |build, transition|
next if transition.loopback?
next unless build.project
- next unless build.update_deployment_after_transaction_commit?
build.run_after_commit do
build.deployment&.sync_status_with(build)
@@ -585,7 +540,6 @@ module Ci
.concat(persisted_variables)
.concat(dependency_proxy_variables)
.concat(job_jwt_variables)
- .concat(kubernetes_variables)
.concat(scoped_variables)
.concat(job_variables)
.concat(persisted_environment_variables)
@@ -1120,12 +1074,6 @@ module Ci
runner&.instance_type?
end
- def update_deployment_after_transaction_commit?
- strong_memoize(:update_deployment_after_transaction_commit) do
- Feature.enabled?(:update_deployment_after_transaction_commit, project, default_enabled: :yaml)
- end
- end
-
protected
def run_status_commit_hooks!
@@ -1213,10 +1161,6 @@ module Ci
end
end
- def kubernetes_variables
- [] # Overridden in EE
- end
-
def conditionally_allow_failure!(exit_code)
return unless exit_code
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 0d6d6f7a6a5..ca68989002c 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -23,6 +23,7 @@ module Ci
serialize :config_options, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
serialize :config_variables, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :runtime_runner_features, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
chronic_duration_attr_reader :timeout_human_readable, :timeout
@@ -37,8 +38,7 @@ module Ci
job_timeout_source: 4
}
- ignore_column :build_id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
- ignore_columns :id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
+ ignore_columns :runner_features, remove_with: '14.7', remove_after: '2021-11-22'
def update_timeout_state
timeout = timeout_with_highest_precedence
@@ -48,6 +48,14 @@ module Ci
update(timeout: timeout.value, timeout_source: timeout.source)
end
+ def set_cancel_gracefully
+ runtime_runner_features.merge!( { cancel_gracefully: true } )
+ end
+
+ def cancel_gracefully?
+ runtime_runner_features[:cancel_gracefully] == true
+ end
+
private
def set_build_project
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index bf1470ca20f..d4cbbfac4ab 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -3,7 +3,6 @@
module Ci
class BuildNeed < Ci::ApplicationRecord
include BulkInsertSafe
- include IgnorableColumns
belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs
@@ -13,12 +12,5 @@ module Ci
scope :scoped_build, -> { where('ci_builds.id=ci_build_needs.build_id') }
scope :artifacts, -> { where(artifacts: true) }
-
- # TODO: Remove once build_id_convert_to_bigint is not an "ignored" column anymore (see .ignore_columns above)
- # There is a database-side trigger to populate this column. This is unexpected in the context
- # of cloning an instance, e.g. when retrying the job. Hence we exclude the ignored column explicitly here.
- def attributes
- super.except('build_id_convert_to_bigint')
- end
end
end
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index e12c0f82c99..c6dbb5d0a43 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -4,8 +4,6 @@ module Ci
# The purpose of this class is to store Build related runner session.
# Data will be removed after transitioning from running to any state.
class BuildRunnerSession < Ci::ApplicationRecord
- include IgnorableColumns
-
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'
DEFAULT_SERVICE_NAME = 'build'
DEFAULT_PORT_NAME = 'default_port'
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 6edb5ef4579..221a2284106 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -7,7 +7,6 @@ module Ci
include ::Checksummable
include ::Gitlab::ExclusiveLeaseHelpers
include ::Gitlab::OptimisticLocking
- include IgnorableColumns
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
diff --git a/app/models/ci/ci_database_record.rb b/app/models/ci/ci_database_record.rb
deleted file mode 100644
index e2b832a28e7..00000000000
--- a/app/models/ci/ci_database_record.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- # TODO: https://gitlab.com/groups/gitlab-org/-/epics/6168
- #
- # Do not use this yet outside of `ci_instance_variables`.
- # This class is part of a migration to move all CI classes to a new separate database.
- # Initially we are only going to be moving the `Ci::InstanceVariable` model and it will be duplicated in the main and CI tables
- # Do not extend this class in any other models.
- class CiDatabaseRecord < Ci::ApplicationRecord
- self.abstract_class = true
-
- if Gitlab::Database.has_config?(:ci)
- connects_to database: { writing: :ci, reading: :ci }
- end
- end
-end
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
index f4aa935b983..da9d4dea537 100644
--- a/app/models/ci/instance_variable.rb
+++ b/app/models/ci/instance_variable.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Ci
- class InstanceVariable < Ci::CiDatabaseRecord
+ class InstanceVariable < Ci::ApplicationRecord
extend Gitlab::ProcessMemoryCache::Helper
include Ci::NewHasVariable
include Ci::Maskable
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index ad3e867f9d5..ec1137920ef 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -10,9 +10,7 @@ module Ci
include Artifactable
include FileStoreMounter
include EachBatch
- include IgnorableColumns
-
- ignore_columns %i[id_convert_to_bigint job_id_convert_to_bigint], remove_with: '14.5', remove_after: '2021-11-22'
+ include Gitlab::Utils::StrongMemoize
TEST_REPORT_FILE_TYPES = %w[junit].freeze
COVERAGE_REPORT_FILE_TYPES = %w[cobertura].freeze
@@ -124,6 +122,9 @@ module Ci
mount_file_store_uploader JobArtifactUploader
+ skip_callback :save, :after, :store_file!, if: :store_after_commit?
+ after_commit :store_file_after_commit!, on: [:create, :update], if: :store_after_commit?
+
validates :file_format, presence: true, unless: :trace?, on: :create
validate :validate_file_format!, unless: :trace?, on: :create
before_save :set_size, if: :file_changed?
@@ -338,8 +339,23 @@ module Ci
}
end
+ def store_after_commit?
+ strong_memoize(:store_after_commit) do
+ trace? &&
+ JobArtifactUploader.direct_upload_enabled? &&
+ Feature.enabled?(:ci_store_trace_outside_transaction, project, default_enabled: :yaml)
+ end
+ end
+
private
+ def store_file_after_commit!
+ return unless previous_changes.key?(:file)
+
+ store_file!
+ update_file_store
+ end
+
def set_size
self.size = file.size
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 0041ec5135c..a29aa756e38 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -71,7 +71,7 @@ module Ci
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
has_many :deployments, through: :builds
- has_many :environments, -> { distinct }, through: :deployments
+ has_many :environments, -> { distinct.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338658') }, through: :deployments
has_many :latest_builds, -> { latest.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
has_many :downloadable_artifacts, -> do
not_expired.or(where_exists(::Ci::Pipeline.artifacts_locked.where('ci_pipelines.id = ci_builds.commit_id'))).downloadable.with_job
@@ -652,8 +652,15 @@ module Ci
end
def batch_lookup_report_artifact_for_file_type(file_type)
+ batch_lookup_report_artifact_for_file_types([file_type])
+ end
+
+ def batch_lookup_report_artifact_for_file_types(file_types)
+ file_types_to_search = []
+ file_types.each { |file_type| file_types_to_search.append(*::Ci::JobArtifact.associated_file_types_for(file_type.to_s)) }
+
latest_report_artifacts
- .values_at(*::Ci::JobArtifact.associated_file_types_for(file_type.to_s))
+ .values_at(*file_types_to_search.uniq)
.flatten
.compact
.last
@@ -684,7 +691,9 @@ module Ci
end
def freeze_period?
- Ci::FreezePeriodStatus.new(project: project).execute
+ strong_memoize(:freeze_period) do
+ Ci::FreezePeriodStatus.new(project: project).execute
+ end
end
def has_warnings?
@@ -780,6 +789,10 @@ module Ci
strong_memoize(:legacy_trigger) { trigger_requests.first }
end
+ def variables_builder
+ @variables_builder ||= ::Gitlab::Ci::Variables::Builder.new(self)
+ end
+
def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless persisted?
@@ -796,20 +809,7 @@ module Ci
variables.append(key: 'CI_PIPELINE_CREATED_AT', value: created_at&.iso8601)
variables.concat(predefined_commit_variables)
-
- if merge_request?
- variables.append(key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: merge_request_event_type.to_s)
- variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', value: source_sha.to_s)
- variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s)
-
- diff = self.merge_request_diff
- if diff.present?
- variables.append(key: 'CI_MERGE_REQUEST_DIFF_ID', value: diff.id.to_s)
- variables.append(key: 'CI_MERGE_REQUEST_DIFF_BASE_SHA', value: diff.base_commit_sha)
- end
-
- variables.concat(merge_request.predefined_variables)
- end
+ variables.concat(predefined_merge_request_variables)
if open_merge_requests_refs.any?
variables.append(key: 'CI_OPEN_MERGE_REQUESTS', value: open_merge_requests_refs.join(','))
@@ -825,27 +825,49 @@ module Ci
end
def predefined_commit_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI_COMMIT_SHA', value: sha)
- variables.append(key: 'CI_COMMIT_SHORT_SHA', value: short_sha)
- variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha)
- variables.append(key: 'CI_COMMIT_REF_NAME', value: source_ref)
- variables.append(key: 'CI_COMMIT_REF_SLUG', value: source_ref_slug)
- variables.append(key: 'CI_COMMIT_BRANCH', value: ref) if branch?
- variables.append(key: 'CI_COMMIT_TAG', value: ref) if tag?
- variables.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
- variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
- variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
- variables.append(key: 'CI_COMMIT_REF_PROTECTED', value: (!!protected_ref?).to_s)
- variables.append(key: 'CI_COMMIT_TIMESTAMP', value: git_commit_timestamp.to_s)
- variables.append(key: 'CI_COMMIT_AUTHOR', value: git_author_full_text.to_s)
-
- # legacy variables
- variables.append(key: 'CI_BUILD_REF', value: sha)
- variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
- variables.append(key: 'CI_BUILD_REF_NAME', value: source_ref)
- variables.append(key: 'CI_BUILD_REF_SLUG', value: source_ref_slug)
- variables.append(key: 'CI_BUILD_TAG', value: ref) if tag?
+ strong_memoize(:predefined_commit_variables) do
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_COMMIT_SHA', value: sha)
+ variables.append(key: 'CI_COMMIT_SHORT_SHA', value: short_sha)
+ variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha)
+ variables.append(key: 'CI_COMMIT_REF_NAME', value: source_ref)
+ variables.append(key: 'CI_COMMIT_REF_SLUG', value: source_ref_slug)
+ variables.append(key: 'CI_COMMIT_BRANCH', value: ref) if branch?
+ variables.append(key: 'CI_COMMIT_TAG', value: ref) if tag?
+ variables.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
+ variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
+ variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
+ variables.append(key: 'CI_COMMIT_REF_PROTECTED', value: (!!protected_ref?).to_s)
+ variables.append(key: 'CI_COMMIT_TIMESTAMP', value: git_commit_timestamp.to_s)
+ variables.append(key: 'CI_COMMIT_AUTHOR', value: git_author_full_text.to_s)
+
+ # legacy variables
+ variables.append(key: 'CI_BUILD_REF', value: sha)
+ variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
+ variables.append(key: 'CI_BUILD_REF_NAME', value: source_ref)
+ variables.append(key: 'CI_BUILD_REF_SLUG', value: source_ref_slug)
+ variables.append(key: 'CI_BUILD_TAG', value: ref) if tag?
+ end
+ end
+ end
+
+ def predefined_merge_request_variables
+ strong_memoize(:predefined_merge_request_variables) do
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ next variables unless merge_request?
+
+ variables.append(key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: merge_request_event_type.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', value: source_sha.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s)
+
+ diff = self.merge_request_diff
+ if diff.present?
+ variables.append(key: 'CI_MERGE_REQUEST_DIFF_ID', value: diff.id.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_DIFF_BASE_SHA', value: diff.base_commit_sha)
+ end
+
+ variables.concat(merge_request.predefined_variables)
+ end
end
end
@@ -1254,6 +1276,18 @@ module Ci
self.builds.latest.build_matchers(project)
end
+ def predefined_vars_in_builder_enabled?
+ strong_memoize(:predefined_vars_in_builder_enabled) do
+ Feature.enabled?(:ci_predefined_vars_in_builder, project, default_enabled: :yaml)
+ end
+ end
+
+ def authorized_cluster_agents
+ strong_memoize(:authorized_cluster_agents) do
+ ::Clusters::AgentAuthorizationsFinder.new(project).execute.map(&:agent)
+ end
+ end
+
private
def add_message(severity, content)
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 2f718ad7582..8a3025e5608 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -12,6 +12,7 @@ module Ci
include Gitlab::Utils::StrongMemoize
include TaggableQueries
include Presentable
+ include LooseForeignKey
add_authentication_token_field :token, encrypted: :optional
@@ -82,7 +83,9 @@ module Ci
groups = Gitlab::ObjectHierarchy.new(groups).base_and_ancestors
end
- joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups })
+ joins(:runner_namespaces)
+ .where(ci_runner_namespaces: { namespace_id: groups })
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
}
scope :belonging_to_group_or_project, -> (group_id, project_id) {
@@ -94,13 +97,16 @@ module Ci
union_sql = ::Gitlab::SQL::Union.new([group_runners, project_runners]).to_sql
from("(#{union_sql}) #{table_name}")
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
}
scope :belonging_to_parent_group_of_project, -> (project_id) {
project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
hierarchy_groups = Gitlab::ObjectHierarchy.new(project_groups).base_and_ancestors
- joins(:groups).where(namespaces: { id: hierarchy_groups })
+ joins(:groups)
+ .where(namespaces: { id: hierarchy_groups })
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
}
scope :owned_or_instance_wide, -> (project_id) do
@@ -111,7 +117,7 @@ module Ci
instance_type
],
remove_duplicates: false
- )
+ ).allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
end
scope :assignable_for, ->(project) do
@@ -162,6 +168,8 @@ module Ci
validates :config, json_schema: { filename: 'ci_runner_config' }
+ loose_foreign_key :clusters_applications_runners, :runner_id, on_delete: :async_nullify
+
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL for the description field and performs a full match on tokens.
@@ -266,6 +274,14 @@ module Ci
end
def status
+ return :not_connected unless contacted_at
+
+ online? ? :online : :offline
+ end
+
+ # DEPRECATED
+ # TODO Remove in %15.0 in favor of `status` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/344648
+ def deprecated_rest_status
if contacted_at.nil?
:not_connected
elsif active?
@@ -436,10 +452,8 @@ module Ci
end
def no_groups
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
- if groups.any?
- errors.add(:runner, 'cannot have groups assigned')
- end
+ if runner_namespaces.any?
+ errors.add(:runner, 'cannot have groups assigned')
end
end
@@ -450,10 +464,8 @@ module Ci
end
def exactly_one_group
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
- unless groups.one?
- errors.add(:runner, 'needs to be assigned to exactly one group')
- end
+ unless runner_namespaces.one?
+ errors.add(:runner, 'needs to be assigned to exactly one group')
end
end
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index 95842d944f9..f78caf710a6 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -4,9 +4,6 @@ module Ci
module Sources
class Pipeline < Ci::ApplicationRecord
include Ci::NamespacedModelName
- include IgnorableColumns
-
- ignore_columns 'source_job_id_convert_to_bigint', remove_with: '14.5', remove_after: '2021-11-22'
self.table_name = "ci_sources_pipelines"
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 131e18adf62..e2b15497638 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -6,7 +6,6 @@ module Ci
include Ci::HasStatus
include Gitlab::OptimisticLocking
include Presentable
- include IgnorableColumns
enum status: Ci::HasStatus::STATUSES_ENUM
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 595315f14ab..5bf5ae51ec8 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -3,6 +3,10 @@
module Ci
class Trigger < Ci::ApplicationRecord
include Presentable
+ include Limitable
+
+ self.limit_name = 'pipeline_triggers'
+ self.limit_scope = :project
belongs_to :project
belongs_to :owner, class_name: "User"
diff --git a/app/models/clusters/agents/group_authorization.rb b/app/models/clusters/agents/group_authorization.rb
index 28a711aaf17..58ba874ab53 100644
--- a/app/models/clusters/agents/group_authorization.rb
+++ b/app/models/clusters/agents/group_authorization.rb
@@ -3,6 +3,8 @@
module Clusters
module Agents
class GroupAuthorization < ApplicationRecord
+ include ::Clusters::Agents::AuthorizationConfigScopes
+
self.table_name = 'agent_group_authorizations'
belongs_to :agent, class_name: 'Clusters::Agent', optional: false
diff --git a/app/models/clusters/agents/project_authorization.rb b/app/models/clusters/agents/project_authorization.rb
index f6d19086751..b9b44741936 100644
--- a/app/models/clusters/agents/project_authorization.rb
+++ b/app/models/clusters/agents/project_authorization.rb
@@ -3,6 +3,8 @@
module Clusters
module Agents
class ProjectAuthorization < ApplicationRecord
+ include ::Clusters::Agents::AuthorizationConfigScopes
+
self.table_name = 'agent_project_authorizations'
belongs_to :agent, class_name: 'Clusters::Agent', optional: false
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 7cef92ce81a..59a9251d6b7 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.31.0'
+ VERSION = '0.34.0'
self.table_name = 'clusters_applications_runners'
@@ -70,7 +70,7 @@ module Clusters
}
if cluster.group_type?
- attributes[:groups] = [group]
+ attributes[:runner_namespaces] = [::Ci::RunnerNamespace.new(namespace: group)]
elsif cluster.project_type?
attributes[:runner_projects] = [::Ci::RunnerProject.new(project: project)]
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index feac7bbc363..87afa9f9491 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -139,8 +139,6 @@ module Clusters
scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) }
scope :with_available_cilium, -> { joins(:application_cilium).merge(::Clusters::Applications::Cilium.available) }
scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct }
- scope :preload_elasticstack, -> { preload(:integration_elastic_stack) }
- scope :preload_environments, -> { preload(:environments) }
scope :managed, -> { where(managed: true) }
scope :with_persisted_applications, -> { eager_load(*APPLICATIONS_ASSOCIATIONS) }
@@ -150,9 +148,7 @@ module Clusters
scope :for_project_namespace, -> (namespace_id) { joins(:projects).where(projects: { namespace_id: namespace_id }) }
scope :with_name, -> (name) { where(name: name) }
- # with_application_prometheus scope is deprecated, and scheduled for removal
- # in %14.0. See https://gitlab.com/groups/gitlab-org/-/epics/4280
- scope :with_application_prometheus, -> { includes(:application_prometheus).joins(:application_prometheus) }
+ scope :with_integration_prometheus, -> { includes(:integration_prometheus).joins(:integration_prometheus) }
scope :with_project_http_integrations, -> (project_ids) do
conditions = { projects: :alert_management_http_integrations }
includes(conditions).joins(conditions).where(projects: { id: project_ids })
@@ -311,7 +307,7 @@ module Clusters
end
def kubeclient
- platform_kubernetes.kubeclient if kubernetes?
+ platform_kubernetes&.kubeclient if kubernetes?
end
def elastic_stack_adapter
diff --git a/app/models/clusters/integrations/prometheus.rb b/app/models/clusters/integrations/prometheus.rb
index d745a49afc1..8b21fa351a3 100644
--- a/app/models/clusters/integrations/prometheus.rb
+++ b/app/models/clusters/integrations/prometheus.rb
@@ -14,6 +14,13 @@ module Clusters
validates :cluster, presence: true
validates :enabled, inclusion: { in: [true, false] }
+ # Periodically checked and kept up to date for Monitor demo projects
+ enum health_status: {
+ unknown: 0,
+ healthy: 1,
+ unhealthy: 2
+ }
+
attr_encrypted :alert_manager_token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_32,
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 43427e2ebc7..d75f7984e2c 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -48,7 +48,7 @@ class CommitStatus < Ci::ApplicationRecord
scope :ordered, -> { order(:name) }
scope :ordered_by_stage, -> { order(stage_idx: :asc) }
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
- scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
+ scope :retried_ordered, -> { retried.order(name: :asc, id: :desc).includes(project: :namespace) }
scope :ordered_by_pipeline, -> { order(pipeline_id: :asc) }
scope :before_stage, -> (index) { where('stage_idx < ?', index) }
scope :for_stage, -> (index) { where(stage_idx: index) }
diff --git a/app/models/concerns/alert_event_lifecycle.rb b/app/models/concerns/alert_event_lifecycle.rb
index 4d2b717ead2..72fe7757b44 100644
--- a/app/models/concerns/alert_event_lifecycle.rb
+++ b/app/models/concerns/alert_event_lifecycle.rb
@@ -41,8 +41,6 @@ module AlertEventLifecycle
scope :firing, -> { where(status: status_value_for(:firing)) }
scope :resolved, -> { where(status: status_value_for(:resolved)) }
- scope :count_by_project_id, -> { group(:project_id).count }
-
def self.status_value_for(name)
state_machines[:status].states[name].value
end
diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
index 7462e1e828b..324e0fb57cb 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
@@ -5,6 +5,23 @@ module Analytics
module StageEventModel
extend ActiveSupport::Concern
+ included do
+ scope :by_stage_event_hash_id, ->(id) { where(stage_event_hash_id: id) }
+ scope :by_project_id, ->(id) { where(project_id: id) }
+ scope :by_group_id, ->(id) { where(group_id: id) }
+ scope :end_event_timestamp_after, -> (date) { where(arel_table[:end_event_timestamp].gteq(date)) }
+ scope :end_event_timestamp_before, -> (date) { where(arel_table[:end_event_timestamp].lteq(date)) }
+ scope :start_event_timestamp_after, -> (date) { where(arel_table[:start_event_timestamp].gteq(date)) }
+ scope :start_event_timestamp_before, -> (date) { where(arel_table[:start_event_timestamp].lteq(date)) }
+ scope :authored, ->(user) { where(author_id: user) }
+ scope :with_milestone_id, ->(milestone_id) { where(milestone_id: milestone_id) }
+ scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) }
+ end
+
+ def issuable_id
+ attributes[self.class.issuable_id_column.to_s]
+ end
+
class_methods do
def upsert_data(data)
upsert_values = data.map do |row|
@@ -13,8 +30,9 @@ module Analytics
:issuable_id,
:group_id,
:project_id,
- :author_id,
:milestone_id,
+ :author_id,
+ :state_id,
:start_event_timestamp,
:end_event_timestamp
)
@@ -31,6 +49,7 @@ module Analytics
project_id,
milestone_id,
author_id,
+ state_id,
start_event_timestamp,
end_event_timestamp
)
@@ -39,10 +58,11 @@ module Analytics
DO UPDATE SET
group_id = excluded.group_id,
project_id = excluded.project_id,
- start_event_timestamp = excluded.start_event_timestamp,
- end_event_timestamp = excluded.end_event_timestamp,
milestone_id = excluded.milestone_id,
- author_id = excluded.author_id
+ author_id = excluded.author_id,
+ state_id = excluded.state_id,
+ start_event_timestamp = excluded.start_event_timestamp,
+ end_event_timestamp = excluded.end_event_timestamp
SQL
result = connection.execute(query)
diff --git a/app/models/concerns/cascading_namespace_setting_attribute.rb b/app/models/concerns/cascading_namespace_setting_attribute.rb
index e58e5ddc966..731729a1ed5 100644
--- a/app/models/concerns/cascading_namespace_setting_attribute.rb
+++ b/app/models/concerns/cascading_namespace_setting_attribute.rb
@@ -127,7 +127,7 @@ module CascadingNamespaceSettingAttribute
end
def alias_boolean(attribute)
- return unless Gitlab::Database.main.exists? && type_for_attribute(attribute).type == :boolean
+ return unless database.exists? && type_for_attribute(attribute).type == :boolean
alias_method :"#{attribute}?", attribute
end
@@ -176,10 +176,10 @@ module CascadingNamespaceSettingAttribute
private
def locked_value(attribute)
+ return application_setting_value(attribute) if locked_by_application_setting?(attribute)
+
ancestor = locked_ancestor(attribute)
return ancestor.read_attribute(attribute) if ancestor
-
- Gitlab::CurrentSettings.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
end
def locked_ancestor(attribute)
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index 27a704c1de0..a9589cea5e9 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -10,11 +10,14 @@ module Ci
# Variables in the environment name scope.
#
def scoped_variables(environment: expanded_environment_name, dependencies: true)
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.concat(predefined_variables)
+ track_duration do
+ variables = pipeline.variables_builder.scoped_variables(self, environment: environment, dependencies: dependencies)
+
+ variables.concat(predefined_variables) unless pipeline.predefined_vars_in_builder_enabled?
variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runnable? && runner
+ variables.concat(kubernetes_variables)
variables.concat(deployment_variables(environment: environment))
variables.concat(yaml_variables)
variables.concat(user_variables)
@@ -25,9 +28,23 @@ module Ci
variables.concat(trigger_request.user_variables) if trigger_request
variables.concat(pipeline.variables)
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
+
+ variables
end
end
+ def track_duration
+ start_time = ::Gitlab::Metrics::System.monotonic_time
+ result = yield
+ duration = ::Gitlab::Metrics::System.monotonic_time - start_time
+
+ ::Gitlab::Ci::Pipeline::Metrics
+ .pipeline_builder_scoped_variables_histogram
+ .observe({}, duration.seconds)
+
+ result
+ end
+
##
# Variables that do not depend on the environment name.
#
@@ -72,6 +89,18 @@ module Ci
end
end
+ def kubernetes_variables
+ ::Gitlab::Ci::Variables::Collection.new.tap do |collection|
+ # Should get merged with the cluster kubeconfig in deployment_variables, see
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/335089
+ template = ::Ci::GenerateKubeconfigService.new(self).execute
+
+ if template.valid?
+ collection.append(key: 'KUBECONFIG', value: template.to_yaml, public: false, file: true)
+ end
+ end
+ end
+
def deployment_variables(environment:)
return [] unless environment
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index 8d715279da8..ccaccec3b6b 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -14,21 +14,8 @@ module Ci
PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze
EXCLUDE_IGNORED_STATUSES = %w[manual failed canceled].to_set.freeze
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
- failed: 4, canceled: 5, skipped: 6, manual: 7,
- scheduled: 8, preparing: 9, waiting_for_resource: 10 }.freeze
- STATUSES_DESCRIPTION = {
- created: 'Pipeline has been created',
- waiting_for_resource: 'A resource (for example, a runner) that the pipeline requires to run is unavailable',
- preparing: 'Pipeline is preparing to run',
- pending: 'Pipeline has not started running yet',
- running: 'Pipeline is running',
- failed: 'At least one stage of the pipeline failed',
- success: 'Pipeline completed successfully',
- canceled: 'Pipeline was canceled before completion',
- skipped: 'Pipeline was skipped',
- manual: 'Pipeline needs to be manually started',
- scheduled: 'Pipeline is scheduled to run'
- }.freeze
+ failed: 4, canceled: 5, skipped: 6, manual: 7,
+ scheduled: 8, preparing: 9, waiting_for_resource: 10 }.freeze
UnknownStatusError = Class.new(StandardError)
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index 344f5aa4cd5..611b27c722b 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -20,7 +20,8 @@ module Ci
delegate :interruptible, to: :metadata, prefix: false, allow_nil: true
delegate :has_exposed_artifacts?, to: :metadata, prefix: false, allow_nil: true
delegate :environment_auto_stop_in, to: :metadata, prefix: false, allow_nil: true
- delegate :runner_features, to: :metadata, prefix: false, allow_nil: false
+ delegate :set_cancel_gracefully, to: :metadata, prefix: false, allow_nil: false
+ delegate :cancel_gracefully?, to: :metadata, prefix: false, allow_nil: false
before_create :ensure_metadata
end
diff --git a/app/models/concerns/clusters/agents/authorization_config_scopes.rb b/app/models/concerns/clusters/agents/authorization_config_scopes.rb
new file mode 100644
index 00000000000..0a0406c3389
--- /dev/null
+++ b/app/models/concerns/clusters/agents/authorization_config_scopes.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Agents
+ module AuthorizationConfigScopes
+ extend ActiveSupport::Concern
+
+ included do
+ scope :with_available_ci_access_fields, ->(project) {
+ where("config->'access_as' IS NULL")
+ .or(where("config->'access_as' = '{}'"))
+ .or(where("config->'access_as' ?| array[:fields]", fields: available_ci_access_fields(project)))
+ }
+ end
+
+ class_methods do
+ def available_ci_access_fields(_project)
+ %w(agent)
+ end
+ end
+ end
+ end
+end
+
+Clusters::Agents::AuthorizationConfigScopes.prepend_mod
diff --git a/app/models/concerns/database_reflection.rb b/app/models/concerns/database_reflection.rb
new file mode 100644
index 00000000000..1842f5bf4ec
--- /dev/null
+++ b/app/models/concerns/database_reflection.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# A module that makes it easier/less verbose to reflect upon a database
+# connection.
+#
+# Using this module you can write this:
+#
+# User.database.database_name
+#
+# Instead of this:
+#
+# Gitlab::Database::Reflection.new(User).database_name
+module DatabaseReflection
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def database
+ @database_reflection ||= ::Gitlab::Database::Reflection.new(self)
+ end
+ end
+end
diff --git a/app/models/concerns/enums/vulnerability.rb b/app/models/concerns/enums/vulnerability.rb
index 749d1ad65cd..4b325de61bc 100644
--- a/app/models/concerns/enums/vulnerability.rb
+++ b/app/models/concerns/enums/vulnerability.rb
@@ -37,6 +37,15 @@ module Enums
security_audit: 4
}.with_indifferent_access.freeze
+ # keep the order of the values in the state enum, it is used in state_order method to properly order vulnerabilities based on state
+ # remember to recreate index_vulnerabilities_on_state_case_id index when you update or extend this enum
+ VULNERABILITY_STATES = {
+ detected: 1,
+ confirmed: 4,
+ resolved: 3,
+ dismissed: 2
+ }.with_indifferent_access.freeze
+
def self.confidence_levels
CONFIDENCE_LEVELS
end
@@ -52,6 +61,10 @@ module Enums
def self.detection_methods
DETECTION_METHODS
end
+
+ def self.vulnerability_states
+ VULNERABILITY_STATES
+ end
end
end
diff --git a/app/models/concerns/file_store_mounter.rb b/app/models/concerns/file_store_mounter.rb
index 9d4463e5297..bfcf8a1e7b9 100644
--- a/app/models/concerns/file_store_mounter.rb
+++ b/app/models/concerns/file_store_mounter.rb
@@ -7,15 +7,13 @@ module FileStoreMounter
def mount_file_store_uploader(uploader)
mount_uploader(:file, uploader)
+ # This hook is a no-op when the file is uploaded after_commit
after_save :update_file_store, if: :saved_change_to_file?
end
end
- private
-
def update_file_store
- # The file.object_store is set during `uploader.store!`
- # which happens after object is inserted/updated
- self.update_column(:file_store, file.object_store)
+ # The file.object_store is set during `uploader.store!` and `uploader.migrate!`
+ update_column(:file_store, file.object_store)
end
end
diff --git a/app/models/concerns/has_integrations.rb b/app/models/concerns/has_integrations.rb
deleted file mode 100644
index 76e03d68600..00000000000
--- a/app/models/concerns/has_integrations.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module HasIntegrations
- extend ActiveSupport::Concern
-
- class_methods do
- def without_integration(integration)
- integrations = Integration
- .select('1')
- .where("#{Integration.table_name}.project_id = projects.id")
- .where(type: integration.type)
-
- Project
- .where('NOT EXISTS (?)', integrations)
- .where(pending_delete: false)
- .where(archived: false)
- end
- end
-end
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
index 4b4f9c0df84..28ee54afaa9 100644
--- a/app/models/concerns/has_user_type.rb
+++ b/app/models/concerns/has_user_type.rb
@@ -28,6 +28,7 @@ module HasUserType
scope :non_internal, -> { humans.or(where(user_type: NON_INTERNAL_USER_TYPES)) }
scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) }
scope :without_project_bot, -> { humans.or(where.not(user_type: :project_bot)) }
+ scope :human_or_service_user, -> { humans.or(where(user_type: :service_user)) }
enum user_type: USER_TYPES
diff --git a/app/models/concerns/integrations/push_data_validations.rb b/app/models/concerns/integrations/push_data_validations.rb
new file mode 100644
index 00000000000..966fc94e289
--- /dev/null
+++ b/app/models/concerns/integrations/push_data_validations.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# This concern is used by registered integrations such as Integrations::TeamCity and
+# Integrations::DroneCi and adds methods to perform validations on the received
+# data.
+module Integrations
+ module PushDataValidations
+ extend ActiveSupport::Concern
+
+ def merge_request_valid?(data)
+ data.dig(:object_attributes, :state) == 'opened' && merge_request_unchecked?(data)
+ end
+
+ def push_valid?(data)
+ data[:total_commits_count] > 0 &&
+ !branch_removed?(data) &&
+ # prefer merge request trigger over push to avoid double builds
+ !opened_merge_requests?(data)
+ end
+
+ def tag_push_valid?(data)
+ data[:total_commits_count] > 0 && !branch_removed?(data)
+ end
+
+ private
+
+ def branch_removed?(data)
+ Gitlab::Git.blank_ref?(data[:after])
+ end
+
+ def opened_merge_requests?(data)
+ project.merge_requests
+ .opened
+ .from_project(project)
+ .from_source_branches(Gitlab::Git.ref_name(data[:ref]))
+ .exists?
+ end
+
+ def merge_request_unchecked?(data)
+ MergeRequest.state_machines[:merge_status]
+ .check_state?(data.dig(:object_attributes, :merge_status))
+ end
+ end
+end
diff --git a/app/models/concerns/integrations/reactively_cached.rb b/app/models/concerns/integrations/reactively_cached.rb
new file mode 100644
index 00000000000..62eff06c8e2
--- /dev/null
+++ b/app/models/concerns/integrations/reactively_cached.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ReactivelyCached
+ extend ActiveSupport::Concern
+
+ included do
+ include ::ReactiveCaching
+
+ # Default cache key: class name + project_id
+ self.reactive_cache_key = ->(integration) { [integration.class.model_name.singular, integration.project_id] }
+ self.reactive_cache_work_type = :external_dependency
+ end
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5c307158a9a..4273eb331a1 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -92,7 +92,6 @@ module Issuable
scope :recent, -> { reorder(id: :desc) }
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :opened, -> { with_state(:opened) }
- scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) }
# rubocop:disable GitlabSecurity/SqlInjection
diff --git a/app/models/concerns/legacy_bulk_insert.rb b/app/models/concerns/legacy_bulk_insert.rb
new file mode 100644
index 00000000000..1249dfb70cd
--- /dev/null
+++ b/app/models/concerns/legacy_bulk_insert.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module LegacyBulkInsert
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Bulk inserts a number of rows into a table, optionally returning their
+ # IDs.
+ #
+ # This method is deprecated, and you should use the BulkInsertSafe module
+ # instead.
+ #
+ # table - The name of the table to insert the rows into.
+ # rows - An Array of Hash instances, each mapping the columns to their
+ # values.
+ # return_ids - When set to true the return value will be an Array of IDs of
+ # the inserted rows
+ # disable_quote - A key or an Array of keys to exclude from quoting (You
+ # become responsible for protection from SQL injection for
+ # these keys!)
+ # on_conflict - Defines an upsert. Values can be: :disabled (default) or
+ # :do_nothing
+ def legacy_bulk_insert(table, rows, return_ids: false, disable_quote: [], on_conflict: nil)
+ return if rows.empty?
+
+ keys = rows.first.keys
+ columns = keys.map { |key| connection.quote_column_name(key) }
+
+ disable_quote = Array(disable_quote).to_set
+ tuples = rows.map do |row|
+ keys.map do |k|
+ disable_quote.include?(k) ? row[k] : connection.quote(row[k])
+ end
+ end
+
+ sql = <<-EOF
+ INSERT INTO #{table} (#{columns.join(', ')})
+ VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
+ EOF
+
+ sql = "#{sql} ON CONFLICT DO NOTHING" if on_conflict == :do_nothing
+
+ sql = "#{sql} RETURNING id" if return_ids
+
+ result = connection.execute(sql)
+
+ if return_ids
+ result.values.map { |tuple| tuple[0].to_i }
+ else
+ []
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb
index 848ef63f1c2..98f6ad58434 100644
--- a/app/models/concerns/loaded_in_group_list.rb
+++ b/app/models/concerns/loaded_in_group_list.rb
@@ -41,9 +41,11 @@ module LoadedInGroupList
namespaces = Namespace.arel_table
children = namespaces.alias('children')
+ # TODO 6473: remove the filtering of the Namespaces::ProjectNamespace see https://gitlab.com/groups/gitlab-org/-/epics/6473
namespaces.project(Arel.star.count.as('preloaded_subgroup_count'))
.from(children)
.where(children[:parent_id].eq(namespaces[:id]))
+ .where(children[:type].is_distinct_from(Namespaces::ProjectNamespace.sti_name))
end
def member_count_sql
diff --git a/app/models/concerns/loose_foreign_key.rb b/app/models/concerns/loose_foreign_key.rb
index 4e822a04869..102292672b3 100644
--- a/app/models/concerns/loose_foreign_key.rb
+++ b/app/models/concerns/loose_foreign_key.rb
@@ -7,20 +7,18 @@ module LooseForeignKey
# Loose foreign keys allow delayed processing of associated database records
# with similar guarantees than a database foreign key.
#
- # TODO: finalize this later once the async job is in place
- #
# Prerequisites:
#
# To start using the concern, you'll need to install a database trigger to the parent
# table in a standard DB migration (not post-migration).
#
- # > add_loose_foreign_key_support(:projects, :gitlab_main)
+ # > track_record_deletions(:projects)
#
# Usage:
#
# > class Ci::Build < ApplicationRecord
# >
- # > loose_foreign_key :security_scans, :build_id, on_delete: :async_delete, gitlab_schema: :gitlab_main
+ # > loose_foreign_key :security_scans, :build_id, on_delete: :async_delete
# >
# > # associations can be still defined, the dependent options is no longer necessary:
# > has_many :security_scans, class_name: 'Security::Scan'
@@ -32,14 +30,6 @@ module LooseForeignKey
# - :async_delete - deletes the children rows via an asynchronous process.
# - :async_nullify - sets the foreign key column to null via an asynchronous process.
#
- # Options for gitlab_schema:
- #
- # - :gitlab_ci
- # - :gitlab_main
- #
- # The value can be determined by calling `Model.gitlab_schema` where the Model represents
- # the model for the child table.
- #
# How it works:
#
# When adding loose foreign key support to the table, a DELETE trigger is installed
@@ -69,23 +59,17 @@ module LooseForeignKey
end
on_delete_options = %i[async_delete async_nullify]
- gitlab_schema_options = [ApplicationRecord.gitlab_schema, Ci::ApplicationRecord.gitlab_schema]
unless on_delete_options.include?(symbolized_options[:on_delete]&.to_sym)
raise "Invalid on_delete option given: #{symbolized_options[:on_delete]}. Valid options: #{on_delete_options.join(', ')}"
end
- unless gitlab_schema_options.include?(symbolized_options[:gitlab_schema]&.to_sym)
- raise "Invalid gitlab_schema option given: #{symbolized_options[:gitlab_schema]}. Valid options: #{gitlab_schema_options.join(', ')}"
- end
-
definition = ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
table_name.to_s,
to_table.to_s,
{
column: column.to_s,
- on_delete: symbolized_options[:on_delete].to_sym,
- gitlab_schema: symbolized_options[:gitlab_schema].to_sym
+ on_delete: symbolized_options[:on_delete].to_sym
}
)
diff --git a/app/models/concerns/merge_request_reviewer_state.rb b/app/models/concerns/merge_request_reviewer_state.rb
new file mode 100644
index 00000000000..216a3a0bd64
--- /dev/null
+++ b/app/models/concerns/merge_request_reviewer_state.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module MergeRequestReviewerState
+ extend ActiveSupport::Concern
+
+ included do
+ enum state: {
+ unreviewed: 0,
+ reviewed: 1,
+ attention_requested: 2
+ }
+
+ validates :state,
+ presence: true,
+ inclusion: { in: self.states.keys }
+
+ after_initialize :set_state, unless: :persisted?
+
+ def set_state
+ if Feature.enabled?(:mr_attention_requests, self.merge_request&.project, default_enabled: :yaml)
+ self.state = :attention_requested
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index c4f810ab9b1..12041b103f6 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -14,13 +14,12 @@ module Milestoneable
validate :milestone_is_valid
- scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :any_milestone, -> { where.not(milestone_id: nil) }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :without_particular_milestone, ->(title) { left_outer_joins(:milestone).where("milestones.title != ? OR milestone_id IS NULL", title) }
scope :any_release, -> { joins_milestone_releases }
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
- scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not( milestones: { releases: { tag: tag, project_id: project_id } } ) }
+ scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not(milestones: { releases: { tag: tag, project_id: project_id } }) }
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index f6d4e5bd27b..ea4fe5b27dc 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -98,6 +98,27 @@ module Noteable
.order('MIN(created_at), MIN(id)')
end
+ # This does not consider OutOfContextDiscussions in MRs
+ # where notes from commits are overriden so that they have
+ # the same discussion_id
+ def discussion_root_note_ids(notes_filter:)
+ relations = []
+
+ relations << discussion_notes.select(
+ "'notes' AS table_name",
+ 'discussion_id',
+ 'MIN(id) AS id',
+ 'MIN(created_at) AS created_at'
+ ).with_notes_filter(notes_filter)
+ .group(:discussion_id)
+
+ if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
+ relations += synthetic_note_ids_relations
+ end
+
+ Note.from_union(relations, remove_duplicates: false).fresh
+ end
+
def capped_notes_count(max)
notes.limit(max).count
end
@@ -179,6 +200,18 @@ module Noteable
project_email.sub('@', "-#{iid}@")
end
+
+ private
+
+ # Synthetic system notes don't have discussion IDs because these are generated dynamically
+ # in Ruby. These are always root notes anyway so we don't need to group by discussion ID.
+ def synthetic_note_ids_relations
+ [
+ resource_label_events.select("'resource_label_events'", "'NULL'", :id, :created_at),
+ resource_milestone_events.select("'resource_milestone_events'", "'NULL'", :id, :created_at),
+ resource_state_events.select("'resource_state_events'", "'NULL'", :id, :created_at)
+ ]
+ end
end
Noteable.extend(Noteable::ClassMethods)
diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb
deleted file mode 100644
index c444f238944..00000000000
--- a/app/models/concerns/reactive_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module ReactiveService
- extend ActiveSupport::Concern
-
- included do
- include ReactiveCaching
-
- # Default cache key: class name + project_id
- self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
- self.reactive_cache_work_type = :external_dependency
- end
-end
diff --git a/app/models/concerns/security/latest_pipeline_information.rb b/app/models/concerns/security/latest_pipeline_information.rb
new file mode 100644
index 00000000000..87eae3cac68
--- /dev/null
+++ b/app/models/concerns/security/latest_pipeline_information.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Security
+ module LatestPipelineInformation
+ private
+
+ def scanner_enabled?(scan_type)
+ latest_builds_reports.include?(scan_type)
+ end
+
+ def latest_builds_reports(only_successful_builds: false)
+ strong_memoize("latest_builds_reports_#{only_successful_builds}") do
+ builds = latest_security_builds
+ builds = builds.select { |build| build.status == 'success' } if only_successful_builds
+ builds.flat_map do |build|
+ build.options[:artifacts][:reports].keys
+ end
+ end
+ end
+
+ def latest_security_builds
+ return [] unless latest_default_branch_pipeline
+
+ ::Security::SecurityJobsFinder.new(pipeline: latest_default_branch_pipeline).execute +
+ ::Security::LicenseComplianceJobsFinder.new(pipeline: latest_default_branch_pipeline).execute
+ end
+
+ def latest_default_branch_pipeline
+ strong_memoize(:pipeline) { latest_pipeline }
+ end
+
+ def auto_devops_source?
+ latest_default_branch_pipeline&.auto_devops_source?
+ end
+ end
+end
diff --git a/app/models/concerns/service_push_data_validations.rb b/app/models/concerns/service_push_data_validations.rb
deleted file mode 100644
index 451804a2c56..00000000000
--- a/app/models/concerns/service_push_data_validations.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-# This concern is used by registered integrations such as Integrations::TeamCity and
-# Integrations::DroneCi and adds methods to perform validations on the received
-# data.
-
-module ServicePushDataValidations
- extend ActiveSupport::Concern
-
- def merge_request_valid?(data)
- data.dig(:object_attributes, :state) == 'opened' && merge_request_unchecked?(data)
- end
-
- def push_valid?(data)
- data[:total_commits_count] > 0 &&
- !branch_removed?(data) &&
- # prefer merge request trigger over push to avoid double builds
- !opened_merge_requests?(data)
- end
-
- def tag_push_valid?(data)
- data[:total_commits_count] > 0 && !branch_removed?(data)
- end
-
- private
-
- def branch_removed?(data)
- Gitlab::Git.blank_ref?(data[:after])
- end
-
- def opened_merge_requests?(data)
- project.merge_requests
- .opened
- .from_project(project)
- .from_source_branches(Gitlab::Git.ref_name(data[:ref]))
- .exists?
- end
-
- def merge_request_unchecked?(data)
- MergeRequest.state_machines[:merge_status]
- .check_state?(data.dig(:object_attributes, :merge_status))
- end
-end
diff --git a/app/models/concerns/sha256_attribute.rb b/app/models/concerns/sha256_attribute.rb
index 17fda6c806c..3c906642b1a 100644
--- a/app/models/concerns/sha256_attribute.rb
+++ b/app/models/concerns/sha256_attribute.rb
@@ -39,7 +39,7 @@ module Sha256Attribute
end
def database_exists?
- Gitlab::Database.main.exists?
+ database.exists?
end
end
end
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index 27277bc5296..ba7c6c0cd8b 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -32,7 +32,7 @@ module ShaAttribute
end
def database_exists?
- Gitlab::Database.main.exists?
+ database.exists?
end
end
end
diff --git a/app/models/concerns/strip_attribute.rb b/app/models/concerns/strip_attribute.rb
index 1c433a3275e..817a4465f91 100644
--- a/app/models/concerns/strip_attribute.rb
+++ b/app/models/concerns/strip_attribute.rb
@@ -2,7 +2,8 @@
# == Strip Attribute module
#
-# Contains functionality to clean attributes before validation
+# Contains functionality to remove leading and trailing
+# whitespace from the attribute before validation
#
# Usage:
#
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 79cbe225e5a..3fe9d7f4d71 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -11,9 +11,7 @@ module Timebox
include StripAttribute
include FromUnion
- TimeboxStruct = Struct.new(:title, :name, :id) do
- include GlobalID::Identification
-
+ TimeboxStruct = Struct.new(:title, :name, :id, :class_name) do
# Ensure these models match the interface required for exporting
def serializable_hash(_opts = {})
{ title: title, name: name, id: id }
@@ -22,6 +20,10 @@ module Timebox
def self.declarative_policy_class
"TimeboxPolicy"
end
+
+ def to_global_id
+ ::Gitlab::GlobalId.build(self, model_name: class_name, id: id)
+ end
end
# Represents a "No Timebox" state used for filtering Issues and Merge
@@ -33,10 +35,10 @@ module Timebox
included do
# Defines the same constants above, but inside the including class.
- const_set :None, TimeboxStruct.new("No #{self.name}", "No #{self.name}", 0)
- const_set :Any, TimeboxStruct.new("Any #{self.name}", '', -1)
- const_set :Upcoming, TimeboxStruct.new('Upcoming', '#upcoming', -2)
- const_set :Started, TimeboxStruct.new('Started', '#started', -3)
+ const_set :None, TimeboxStruct.new("No #{self.name}", "No #{self.name}", 0, self.name)
+ const_set :Any, TimeboxStruct.new("Any #{self.name}", '', -1, self.name)
+ const_set :Upcoming, TimeboxStruct.new('Upcoming', '#upcoming', -2, self.name)
+ const_set :Started, TimeboxStruct.new('Started', '#started', -3, self.name)
alias_method :timebox_id, :id
diff --git a/app/models/concerns/transactions.rb b/app/models/concerns/transactions.rb
new file mode 100644
index 00000000000..a186ebc8475
--- /dev/null
+++ b/app/models/concerns/transactions.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Transactions
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # inside_transaction? will return true if the caller is running within a
+ # transaction. Handles special cases when running inside a test environment,
+ # where tests may be wrapped in transactions
+ def inside_transaction?
+ base = Rails.env.test? ? @open_transactions_baseline.to_i : 0
+
+ connection.open_transactions > base
+ end
+
+ # These methods that access @open_transactions_baseline are not thread-safe.
+ # These are fine though because we only call these in RSpec's main thread.
+ # If we decide to run specs multi-threaded, we would need to use something
+ # like ThreadGroup to keep track of this value
+ def set_open_transactions_baseline
+ @open_transactions_baseline = connection.open_transactions
+ end
+
+ def reset_open_transactions_baseline
+ @open_transactions_baseline = 0
+ end
+ end
+end
diff --git a/app/models/concerns/ttl_expirable.rb b/app/models/concerns/ttl_expirable.rb
index 00abe0a06e6..6d89521255c 100644
--- a/app/models/concerns/ttl_expirable.rb
+++ b/app/models/concerns/ttl_expirable.rb
@@ -5,10 +5,11 @@ module TtlExpirable
included do
validates :status, presence: true
+ default_value_for :read_at, Time.zone.now
enum status: { default: 0, expired: 1, processing: 2, error: 3 }
- scope :updated_before, ->(number_of_days) { where("updated_at <= ?", Time.zone.now - number_of_days.days) }
+ scope :read_before, ->(number_of_days) { where("read_at <= ?", Time.zone.now - number_of_days.days) }
scope :active, -> { where(status: :default) }
scope :lock_next_by, ->(sort) do
@@ -17,4 +18,8 @@ module TtlExpirable
.lock('FOR UPDATE SKIP LOCKED')
end
end
+
+ def read!
+ self.update(read_at: Time.zone.now)
+ end
end
diff --git a/app/models/concerns/update_highest_role.rb b/app/models/concerns/update_highest_role.rb
index 6432cc794a5..2b0ec5c7e21 100644
--- a/app/models/concerns/update_highest_role.rb
+++ b/app/models/concerns/update_highest_role.rb
@@ -15,7 +15,7 @@ module UpdateHighestRole
# Schedule a Sidekiq job to update the highest role for a User
#
# The job will be called outside of a transaction in order to ensure the changes
- # to be commited before attempting to update the highest role.
+ # to be committed before attempting to update the highest role.
# The exlusive lease will not be released after completion to prevent multiple jobs
# being executed during the defined timeout.
def update_highest_role
diff --git a/app/models/concerns/x509_serial_number_attribute.rb b/app/models/concerns/x509_serial_number_attribute.rb
index dfb1e151b41..e51ed95bf70 100644
--- a/app/models/concerns/x509_serial_number_attribute.rb
+++ b/app/models/concerns/x509_serial_number_attribute.rb
@@ -39,7 +39,7 @@ module X509SerialNumberAttribute
end
def database_exists?
- Gitlab::Database.main.exists?
+ database.exists?
end
end
end
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index ecdac64b31b..173b38b2c63 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -31,7 +31,7 @@ class CustomEmoji < ApplicationRecord
private
def valid_emoji_name
- if Gitlab::Emoji.emoji_exists?(name)
+ if TanukiEmoji.find_by_alpha_code(name)
errors.add(:name, _('%{name} is already being used for another emoji') % { name: self.name })
end
end
diff --git a/app/models/customer_relations/contact.rb b/app/models/customer_relations/contact.rb
index c632f8e2efa..5898bc3412f 100644
--- a/app/models/customer_relations/contact.rb
+++ b/app/models/customer_relations/contact.rb
@@ -7,7 +7,8 @@ class CustomerRelations::Contact < ApplicationRecord
belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'group_id'
belongs_to :organization, optional: true
- has_and_belongs_to_many :issues, join_table: :issue_customer_relations_contacts # rubocop: disable Rails/HasAndBelongsToMany
+ has_many :issue_contacts, inverse_of: :contact
+ has_many :issues, through: :issue_contacts, inverse_of: :customer_relations_contacts
strip_attributes! :phone, :first_name, :last_name
diff --git a/app/models/customer_relations/issue_contact.rb b/app/models/customer_relations/issue_contact.rb
new file mode 100644
index 00000000000..98faf8d6644
--- /dev/null
+++ b/app/models/customer_relations/issue_contact.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class CustomerRelations::IssueContact < ApplicationRecord
+ self.table_name = "issue_customer_relations_contacts"
+
+ belongs_to :issue, optional: false, inverse_of: :customer_relations_contacts
+ belongs_to :contact, optional: false, inverse_of: :issue_contacts
+
+ validate :contact_belongs_to_issue_group
+
+ private
+
+ def contact_belongs_to_issue_group
+ return unless contact&.group_id
+ return unless issue&.project&.namespace_id
+ return if contact.group_id == issue.project.namespace_id
+
+ errors.add(:base, _('The contact does not belong to the same group as the issue'))
+ end
+end
diff --git a/app/models/data_list.rb b/app/models/data_list.rb
index adad8e3013e..e99364b2709 100644
--- a/app/models/data_list.rb
+++ b/app/models/data_list.rb
@@ -1,22 +1,26 @@
# frozen_string_literal: true
class DataList
- def initialize(batch, data_fields_hash, klass)
+ def initialize(batch, data_fields_hash, data_fields_klass)
@batch = batch
@data_fields_hash = data_fields_hash
- @klass = klass
+ @data_fields_klass = data_fields_klass
end
def to_array
- [klass, columns, values]
+ [data_fields_klass, columns, values]
end
private
- attr_reader :batch, :data_fields_hash, :klass
+ attr_reader :batch, :data_fields_hash, :data_fields_klass
def columns
- data_fields_hash.keys << 'service_id'
+ data_fields_hash.keys << data_fields_foreign_key
+ end
+
+ def data_fields_foreign_key
+ data_fields_klass.reflections['integration'].foreign_key
end
def values
diff --git a/app/models/dependency_proxy/blob.rb b/app/models/dependency_proxy/blob.rb
index 7ca15652586..bd5c022e692 100644
--- a/app/models/dependency_proxy/blob.rb
+++ b/app/models/dependency_proxy/blob.rb
@@ -7,6 +7,8 @@ class DependencyProxy::Blob < ApplicationRecord
belongs_to :group
+ MAX_FILE_SIZE = 5.gigabytes.freeze
+
validates :group, presence: true
validates :file, presence: true
validates :file_name, presence: true
diff --git a/app/models/dependency_proxy/manifest.rb b/app/models/dependency_proxy/manifest.rb
index b83047efe54..64f484942ef 100644
--- a/app/models/dependency_proxy/manifest.rb
+++ b/app/models/dependency_proxy/manifest.rb
@@ -7,17 +7,19 @@ class DependencyProxy::Manifest < ApplicationRecord
belongs_to :group
+ MAX_FILE_SIZE = 10.megabytes.freeze
+ DIGEST_HEADER = 'Docker-Content-Digest'
+
validates :group, presence: true
validates :file, presence: true
validates :file_name, presence: true
validates :digest, presence: true
- mount_file_store_uploader DependencyProxy::FileUploader
+ scope :order_id_desc, -> { reorder(id: :desc) }
- def self.find_or_initialize_by_file_name_or_digest(file_name:, digest:)
- result = find_by(file_name: file_name) || find_by(digest: digest)
- return result if result
+ mount_file_store_uploader DependencyProxy::FileUploader
- new(file_name: file_name, digest: digest)
+ def self.find_by_file_name_or_digest(file_name:, digest:)
+ find_by(file_name: file_name) || find_by(digest: digest)
end
end
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 8f5a713af3f..4ed38f578ee 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -7,12 +7,16 @@ class DeployKey < Key
has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :deploy_keys_projects
+
+ has_many :deploy_keys_projects_with_write_access, -> { with_write_access }, class_name: "DeployKeysProject"
+ has_many :projects_with_write_access, -> { includes(:route) }, class_name: 'Project', through: :deploy_keys_projects_with_write_access, source: :project
has_many :protected_branch_push_access_levels, class_name: '::ProtectedBranch::PushAccessLevel'
scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where(deploy_keys_projects: { project_id: projects }) }
scope :with_write_access, -> { joins(:deploy_keys_projects).merge(DeployKeysProject.with_write_access) }
scope :are_public, -> { where(public: true) }
scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, namespace: :route] }) }
+ scope :including_projects_with_write_access, -> { includes(:projects_with_write_access) }
accepts_nested_attributes_for :deploy_keys_projects
@@ -52,10 +56,6 @@ class DeployKey < Key
end
end
- def projects_with_write_access
- Project.with_route.where(id: deploy_keys_projects.with_write_access.select(:project_id))
- end
-
def self.with_write_access_for_project(project, deploy_key: nil)
query = in_projects(project).with_write_access
query = query.where(id: deploy_key) if deploy_key
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index f91700f764b..ade19ce02a8 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -8,11 +8,12 @@ class Deployment < ApplicationRecord
include Importable
include Gitlab::Utils::StrongMemoize
include FastDestroyAll
- include IgnorableColumns
StatusUpdateError = Class.new(StandardError)
StatusSyncError = Class.new(StandardError)
+ ARCHIVABLE_OFFSET = 50_000
+
belongs_to :project, required: true
belongs_to :environment, required: true
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
@@ -54,6 +55,8 @@ class Deployment < ApplicationRecord
scope :finished_after, ->(date) { where('finished_at >= ?', date) }
scope :finished_before, ->(date) { where('finished_at < ?', date) }
+ scope :ordered, -> { order(finished_at: :desc) }
+
FINISHED_STATUSES = %i[success failed canceled].freeze
state_machine :status, initial: :created do
@@ -99,6 +102,10 @@ class Deployment < ApplicationRecord
deployment.run_after_commit do
Deployments::UpdateEnvironmentWorker.perform_async(id)
Deployments::LinkMergeRequestWorker.perform_async(id)
+
+ if ::Feature.enabled?(:deployments_archive, deployment.project, default_enabled: :yaml)
+ Deployments::ArchiveInProjectWorker.perform_async(deployment.project_id)
+ end
end
end
@@ -132,6 +139,14 @@ class Deployment < ApplicationRecord
skipped: 5
}
+ def self.archivables_in(project, limit:)
+ start_iid = project.deployments.order(iid: :desc).limit(1)
+ .select("(iid - #{ARCHIVABLE_OFFSET}) AS start_iid")
+
+ project.deployments.preload(:environment).where('iid <= (?)', start_iid)
+ .where(archived: false).limit(limit)
+ end
+
def self.last_for_environment(environment)
ids = self
.for_environment(environment)
@@ -299,7 +314,7 @@ class Deployment < ApplicationRecord
"#{id} as deployment_id",
"#{environment_id} as environment_id").to_sql
- # We don't use `Gitlab::Database.main.bulk_insert` here so that we don't need to
+ # We don't use `ApplicationRecord.legacy_bulk_insert` here so that we don't need to
# first pluck lots of IDs into memory.
#
# We also ignore any duplicates so this method can be called multiple times
@@ -325,6 +340,7 @@ class Deployment < ApplicationRecord
def sync_status_with(build)
return false unless ::Deployment.statuses.include?(build.status)
+ return false if build.created? || build.status == self.status
update_status!(build.status)
rescue StandardError => e
diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb
index 6cda03557d1..5819404efb9 100644
--- a/app/models/design_management/version.rb
+++ b/app/models/design_management/version.rb
@@ -88,7 +88,7 @@ module DesignManagement
rows = design_actions.map { |action| action.row_attrs(version) }
- Gitlab::Database.main.bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert
version.designs.reset
version.validate!
design_actions.each(&:performed)
diff --git a/app/models/email.rb b/app/models/email.rb
index 0140f784842..676e79406e9 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -29,7 +29,7 @@ class Email < ApplicationRecord
end
def unique_email
- self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
+ self.errors.add(:email, 'has already been taken') if primary_email_of_another_user?
end
def validate_email_format
@@ -40,4 +40,14 @@ class Email < ApplicationRecord
def update_invalid_gpg_signatures
user.update_invalid_gpg_signatures if confirmed?
end
+
+ def user_primary_email?
+ email.casecmp?(user.email)
+ end
+
+ private
+
+ def primary_email_of_another_user?
+ User.where(email: email).where.not(id: user_id).exists?
+ end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 31ab426728b..2618991c9e5 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -425,6 +425,14 @@ class Environment < ApplicationRecord
clear_reactive_cache!
end
+ def should_link_to_merge_requests?
+ unfoldered? || production? || staging?
+ end
+
+ def unfoldered?
+ environment_type.nil?
+ end
+
private
def rollout_status_available?
diff --git a/app/models/error_tracking/error.rb b/app/models/error_tracking/error.rb
index 2d6a4694def..efbb6adff70 100644
--- a/app/models/error_tracking/error.rb
+++ b/app/models/error_tracking/error.rb
@@ -18,9 +18,10 @@ class ErrorTracking::Error < ApplicationRecord
scope :for_status, -> (status) { where(status: status) }
validates :project, presence: true
- validates :name, presence: true
- validates :description, presence: true
- validates :actor, presence: true
+ validates :name, presence: true, length: { maximum: 255 }
+ validates :description, presence: true, length: { maximum: 1024 }
+ validates :actor, presence: true, length: { maximum: 255 }
+ validates :platform, length: { maximum: 255 }
validates :status, presence: true
enum status: {
diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb
index 686518a39fb..0b638f65768 100644
--- a/app/models/error_tracking/error_event.rb
+++ b/app/models/error_tracking/error_event.rb
@@ -6,7 +6,9 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
validates :payload, json_schema: { filename: 'error_tracking_event_payload' }
validates :error, presence: true
- validates :description, presence: true
+ validates :description, presence: true, length: { maximum: 1024 }
+ validates :level, length: { maximum: 255 }
+ validates :environment, length: { maximum: 255 }
validates :occurred_at, presence: true
def stacktrace
@@ -61,9 +63,9 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
pre_context = entry['pre_context']
post_context = entry['post_context']
- context += lines_with_position(pre_context, error_line_no - pre_context.size)
+ context += lines_with_position(pre_context, error_line_no - pre_context.size) if pre_context
context += lines_with_position([error_line], error_line_no)
- context += lines_with_position(post_context, error_line_no + 1)
+ context += lines_with_position(post_context, error_line_no + 1) if post_context
context.reject(&:blank?)
end
diff --git a/app/models/event.rb b/app/models/event.rb
index d6588699d27..f6174589a84 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -9,9 +9,6 @@ class Event < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include UsageStatistics
include ShaAttribute
- include IgnorableColumns
-
- ignore_columns :id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
default_scope { reorder(nil) } # rubocop:disable Cop/DefaultScope
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
index 0c36e51120f..2775b520b2f 100644
--- a/app/models/gpg_signature.rb
+++ b/app/models/gpg_signature.rb
@@ -12,7 +12,8 @@ class GpgSignature < ApplicationRecord
same_user_different_email: 2,
other_user: 3,
unverified_key: 4,
- unknown_key: 5
+ unknown_key: 5,
+ multiple_signatures: 6
}
belongs_to :project
diff --git a/app/models/group.rb b/app/models/group.rb
index c5e119451e3..2dd20300ad2 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -56,6 +56,9 @@ class Group < Namespace
has_many :boards
has_many :badges, class_name: 'GroupBadge'
+ has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group
+ has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group
+
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'
@@ -194,13 +197,8 @@ class Group < Namespace
def ids_with_disabled_email(groups)
inner_groups = Group.where('id = namespaces_with_emails_disabled.id')
- inner_ancestors = if Feature.enabled?(:linear_group_ancestor_scopes, default_enabled: :yaml)
- inner_groups.self_and_ancestors
- else
- Gitlab::ObjectHierarchy.new(inner_groups).base_and_ancestors
- end
-
- inner_query = inner_ancestors
+ inner_query = inner_groups
+ .self_and_ancestors
.where(emails_disabled: true)
.select('1')
.limit(1)
@@ -317,13 +315,15 @@ class Group < Namespace
owners.include?(user)
end
- def add_users(users, access_level, current_user: nil, expires_at: nil)
+ def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
Members::Groups::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
self,
users,
access_level,
current_user: current_user,
- expires_at: expires_at
+ expires_at: expires_at,
+ tasks_to_be_done: tasks_to_be_done,
+ tasks_project_id: tasks_project_id
)
end
@@ -760,18 +760,6 @@ class Group < Namespace
Timelog.in_group(self)
end
- def cached_issues_state_count_enabled?
- Feature.enabled?(:cached_issues_state_count, self, default_enabled: :yaml)
- end
-
- def organizations
- ::CustomerRelations::Organization.where(group_id: self.id)
- end
-
- def contacts
- ::CustomerRelations::Contact.where(group_id: self.id)
- end
-
def dependency_proxy_image_ttl_policy
super || build_dependency_proxy_image_ttl_policy
end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 158764bb783..d3059fa6d4a 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -14,7 +14,7 @@ class Integration < ApplicationRecord
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord
drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
- pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
+ pivotaltracker prometheus pushover redmine shimo slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao
].freeze
PROJECT_SPECIFIC_INTEGRATION_NAMES = %w[
@@ -373,7 +373,7 @@ class Integration < ApplicationRecord
end
def to_data_fields_hash
- data_fields.as_json(only: data_fields.class.column_names).except('id', 'service_id')
+ data_fields.as_json(only: data_fields.class.column_names).except('id', 'service_id', 'integration_id')
end
def event_channel_names
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 1a7cbaa34c7..0774b84b69f 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -3,7 +3,7 @@
module Integrations
class Bamboo < BaseCi
include ActionView::Helpers::UrlHelper
- include ReactiveService
+ include ReactivelyCached
prop_accessor :bamboo_url, :build_key, :username, :password
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index c6335782b5e..ca72de47d30 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -73,7 +73,12 @@ module Integrations
{ type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}", required: true }.freeze,
{ type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze,
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze,
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze,
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ }.freeze,
{
type: 'text',
name: 'labels_to_be_notified',
diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb
index 94a37f0c4f2..9fad3a42647 100644
--- a/app/models/integrations/buildkite.rb
+++ b/app/models/integrations/buildkite.rb
@@ -5,7 +5,7 @@ require "addressable/uri"
module Integrations
class Buildkite < BaseCi
include HasWebHook
- include ReactiveService
+ include ReactivelyCached
extend Gitlab::Utils::Override
ENDPOINT = "https://buildkite.com"
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index 76160a61bc3..21993dd3c43 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -41,7 +41,12 @@ module Integrations
[
{ type: "text", name: "webhook", placeholder: "https://discordapp.com/api/webhooks/…", help: "URL to the webhook for the Discord channel." },
{ type: "checkbox", name: "notify_only_broken_pipelines" },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ }
]
end
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
index c93ae432fe9..856d14c022d 100644
--- a/app/models/integrations/drone_ci.rb
+++ b/app/models/integrations/drone_ci.rb
@@ -3,8 +3,8 @@
module Integrations
class DroneCi < BaseCi
include HasWebHook
- include ReactiveService
- include ServicePushDataValidations
+ include PushDataValidations
+ include ReactivelyCached
extend Gitlab::Utils::Override
prop_accessor :drone_url, :token
diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb
index e277633664f..a9cd67550dc 100644
--- a/app/models/integrations/emails_on_push.rb
+++ b/app/models/integrations/emails_on_push.rb
@@ -76,7 +76,12 @@ module Integrations
help: s_("EmailsOnPushService|Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance (such as %{domains}).") % { domains: domains } },
{ type: 'checkbox', name: 'disable_diffs', title: s_("EmailsOnPushService|Disable code diffs"),
help: s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices },
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ },
{
type: 'textarea',
name: 'recipients',
@@ -92,7 +97,7 @@ module Integrations
return if recipients.blank?
if self.class.valid_recipients(recipients).size > RECIPIENTS_LIMIT
- errors.add(:recipients, s_("EmailsOnPushService|can't exceed %{recipients_limit}") % { recipients_limit: RECIPIENTS_LIMIT })
+ errors.add(:recipients, s_("Integrations|can't exceed %{recipients_limit}") % { recipients_limit: RECIPIENTS_LIMIT })
end
end
end
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index d02cfe4ec56..0d6b9fb1019 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -40,7 +40,12 @@ module Integrations
[
{ type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ }
]
end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index ec6adc87bf4..42c291abf55 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -89,7 +89,6 @@ module Integrations
site: URI.join(url, '/').to_s.delete_suffix('/'), # Intended to find the root
context_path: (url.path.presence || '/').delete_suffix('/'),
auth_type: :basic,
- read_timeout: 120,
use_cookies: true,
additional_cookies: ['OBBasicAuth=fromDialog'],
use_ssl: url.scheme == 'https'
@@ -303,6 +302,14 @@ module Integrations
private
+ def branch_name(noteable)
+ if Feature.enabled?(:jira_use_first_ref_by_oid, project, default_enabled: :yaml)
+ noteable.first_ref_by_oid(project.repository)
+ else
+ noteable.ref_names(project.repository).first
+ end
+ end
+
def server_info
strong_memoize(:server_info) do
client_url.present? ? jira_request { client.ServerInfo.all.attrs } : nil
@@ -496,7 +503,7 @@ module Integrations
{
id: noteable.short_id,
description: noteable.safe_message,
- branch: noteable.ref_names(project.repository).first
+ branch: branch_name(noteable)
}
elsif noteable.is_a?(MergeRequest)
{
@@ -521,7 +528,9 @@ module Integrations
yield
rescue StandardError => error
@error = error
- log_error("Error sending message", client_url: client_url, error: @error.message)
+ payload = { client_url: client_url }
+ Gitlab::ExceptionLogFormatter.format!(error, payload)
+ log_error("Error sending message", payload)
nil
end
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
index 5aad25e8ddc..71cd4ddaf82 100644
--- a/app/models/integrations/microsoft_teams.rb
+++ b/app/models/integrations/microsoft_teams.rb
@@ -37,7 +37,12 @@ module Integrations
[
{ type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'If selected, successful pipelines do not trigger a notification event.' },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ }
]
end
diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb
index efba35cc2a8..6dc41958daa 100644
--- a/app/models/integrations/pipelines_email.rb
+++ b/app/models/integrations/pipelines_email.rb
@@ -4,9 +4,12 @@ module Integrations
class PipelinesEmail < Integration
include NotificationBranchSelection
+ RECIPIENTS_LIMIT = 30
+
prop_accessor :recipients, :branches_to_be_notified
boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :recipients, presence: true, if: :validate_recipients?
+ validate :number_of_recipients_within_limit, if: :validate_recipients?
def initialize_properties
if properties.nil?
@@ -49,7 +52,7 @@ module Integrations
return unless supported_events.include?(data[:object_kind])
return unless force || should_pipeline_be_notified?(data)
- all_recipients = retrieve_recipients(data)
+ all_recipients = retrieve_recipients
return unless all_recipients.any?
@@ -71,6 +74,7 @@ module Integrations
name: 'notify_only_broken_pipelines' },
{ type: 'select',
name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
choices: branch_choices }
]
end
@@ -98,8 +102,18 @@ module Integrations
end
end
- def retrieve_recipients(data)
+ def retrieve_recipients
recipients.to_s.split(/[,\r\n ]+/).reject(&:empty?)
end
+
+ private
+
+ def number_of_recipients_within_limit
+ return if recipients.blank?
+
+ if retrieve_recipients.size > RECIPIENTS_LIMIT
+ errors.add(:recipients, s_("Integrations|can't exceed %{recipients_limit}") % { recipients_limit: RECIPIENTS_LIMIT })
+ end
+ end
end
end
diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb
new file mode 100644
index 00000000000..4f42fda2577
--- /dev/null
+++ b/app/models/integrations/shimo.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Shimo < Integration
+ prop_accessor :external_wiki_url
+ validates :external_wiki_url, presence: true, public_url: true, if: :activated?
+
+ def render?
+ valid? && activated?
+ end
+
+ def title
+ s_('Shimo|Shimo')
+ end
+
+ def description
+ s_('Shimo|Link to a Shimo Workspace from the sidebar.')
+ end
+
+ def self.to_param
+ 'shimo'
+ end
+
+ # support for `test` method
+ def execute(_data)
+ response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true, use_read_total_timeout: true)
+ response.body if response.code == 200
+ rescue StandardError
+ nil
+ end
+
+ def self.supported_events
+ %w()
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'external_wiki_url',
+ title: s_('Shimo|Shimo Workspace URL'),
+ required: true
+ }
+ ]
+ end
+ end
+end
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index 3f868b57597..008b591c304 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -2,8 +2,8 @@
module Integrations
class Teamcity < BaseCi
- include ReactiveService
- include ServicePushDataValidations
+ include PushDataValidations
+ include ReactivelyCached
prop_accessor :teamcity_url, :build_type, :username, :password
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index e3e180ae959..f085423d229 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -34,7 +34,12 @@ module Integrations
[
{ type: 'text', name: 'webhook', placeholder: "https://yourcircuit.com/rest/v2/webhooks/incoming/…", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ }
]
end
diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb
index 6fd82a32035..7660eda6f83 100644
--- a/app/models/integrations/webex_teams.rb
+++ b/app/models/integrations/webex_teams.rb
@@ -36,7 +36,12 @@ module Integrations
[
{ type: 'text', name: 'webhook', placeholder: "https://api.ciscospark.com/v1/webhooks/incoming/...", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ }
]
end
diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb
index 68c02f54c61..493d42cc40b 100644
--- a/app/models/integrations/zentao.rb
+++ b/app/models/integrations/zentao.rb
@@ -2,6 +2,8 @@
module Integrations
class Zentao < Integration
+ include Gitlab::Routing
+
data_field :url, :api_url, :api_token, :zentao_product_xid
validates :url, public_url: true, presence: true, if: :activated?
@@ -9,16 +11,29 @@ module Integrations
validates :api_token, presence: true, if: :activated?
validates :zentao_product_xid, presence: true, if: :activated?
+ # License Level: EEP_FEATURES
+ def self.issues_license_available?(project)
+ project&.licensed_feature_available?(:zentao_issues_integration)
+ end
+
def data_fields
zentao_tracker_data || self.build_zentao_tracker_data
end
def title
- self.class.name.demodulize
+ 'ZenTao'
end
def description
- s_("ZentaoIntegration|Use Zentao as this project's issue tracker.")
+ s_("ZentaoIntegration|Use ZenTao as this project's issue tracker.")
+ end
+
+ def help
+ s_("ZentaoIntegration|Before you enable this integration, you must configure ZenTao. For more details, read the %{link_start}ZenTao integration documentation%{link_end}.") % {
+ link_start: '<a href="%{url}" target="_blank" rel="noopener noreferrer">'
+ .html_safe % { url: help_page_url('user/project/integrations/zentao') },
+ link_end: '</a>'.html_safe
+ }
end
def self.to_param
@@ -42,28 +57,29 @@ module Integrations
{
type: 'text',
name: 'url',
- title: s_('ZentaoIntegration|Zentao Web URL'),
+ title: s_('ZentaoIntegration|ZenTao Web URL'),
placeholder: 'https://www.zentao.net',
- help: s_('ZentaoIntegration|Base URL of the Zentao instance.'),
+ help: s_('ZentaoIntegration|Base URL of the ZenTao instance.'),
required: true
},
{
type: 'text',
name: 'api_url',
- title: s_('ZentaoIntegration|Zentao API URL (optional)'),
+ title: s_('ZentaoIntegration|ZenTao API URL (optional)'),
help: s_('ZentaoIntegration|If different from Web URL.')
},
{
type: 'password',
name: 'api_token',
- title: s_('ZentaoIntegration|Zentao API token'),
- non_empty_password_title: s_('ZentaoIntegration|Enter API token'),
+ title: s_('ZentaoIntegration|ZenTao API token'),
+ non_empty_password_title: s_('ZentaoIntegration|Enter new ZenTao API token'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
required: true
},
{
type: 'text',
name: 'zentao_product_xid',
- title: s_('ZentaoIntegration|Zentao Product ID'),
+ title: s_('ZentaoIntegration|ZenTao Product ID'),
required: true
}
]
@@ -76,3 +92,5 @@ module Integrations
end
end
end
+
+::Integrations::Zentao.prepend_mod
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 9c568414ec2..47dc084d69c 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -81,7 +81,8 @@ class Issue < ApplicationRecord
has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_many :prometheus_alerts, through: :prometheus_alert_events
- has_and_belongs_to_many :customer_relations_contacts, join_table: :issue_customer_relations_contacts, class_name: 'CustomerRelations::Contact' # rubocop: disable Rails/HasAndBelongsToMany
+ has_many :issue_customer_relations_contacts, class_name: 'CustomerRelations::IssueContact', inverse_of: :issue
+ has_many :customer_relations_contacts, through: :issue_customer_relations_contacts, source: :contact, class_name: 'CustomerRelations::Contact', inverse_of: :issues
accepts_nested_attributes_for :issuable_severity, update_only: true
accepts_nested_attributes_for :sentry_issue
@@ -203,6 +204,8 @@ class Issue < ApplicationRecord
before_transition closed: :opened do |issue|
issue.closed_at = nil
issue.closed_by = nil
+
+ issue.clear_closure_reason_references
end
end
@@ -378,6 +381,11 @@ class Issue < ApplicationRecord
!duplicated_to_id.nil?
end
+ def clear_closure_reason_references
+ self.moved_to_id = nil
+ self.duplicated_to_id = nil
+ end
+
def can_move?(user, to_project = nil)
if to_project
return false unless user.can?(:admin_issue, to_project)
diff --git a/app/models/key.rb b/app/models/key.rb
index 64385953865..a478434538c 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -46,7 +46,7 @@ class Key < ApplicationRecord
scope :order_last_used_at_desc, -> { reorder(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
# Date is set specifically in this scope to improve query time.
- scope :expired_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') BETWEEN '2000-01-01' AND CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) }
+ scope :expired_today_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') = CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) }
scope :expiring_soon_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') > CURRENT_DATE AND date(expires_at AT TIME ZONE 'UTC') < ? AND before_expiry_notification_delivered_at IS NULL", DAYS_TO_EXPIRE.days.from_now.to_date]) }
def self.regular_keys
diff --git a/app/models/label_link.rb b/app/models/label_link.rb
index 4fb5fd8c58a..d326b07ad31 100644
--- a/app/models/label_link.rb
+++ b/app/models/label_link.rb
@@ -11,4 +11,16 @@ class LabelLink < ApplicationRecord
validates :label, presence: true, unless: :importing?
scope :for_target, -> (target_id, target_type) { where(target_id: target_id, target_type: target_type) }
+
+ # Example: Issues has at least one label within a project
+ # > Issue.where(project_id: 100) # or any scope on issues
+ # > .where(LabelLink.by_target_for_exists_query('Issue', Issue.arel_table[:id]).arel.exists)
+ scope :by_target_for_exists_query, -> (target_type, arel_join_column, label_ids = nil) do
+ relation = LabelLink
+ .where(target_type: target_type)
+ .where(arel_table['target_id'].eq(arel_join_column))
+
+ relation = relation.where(label_id: label_ids) if label_ids
+ relation
+ end
end
diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb
index ca5a2800a03..c3b3e76f67b 100644
--- a/app/models/loose_foreign_keys/deleted_record.rb
+++ b/app/models/loose_foreign_keys/deleted_record.rb
@@ -1,5 +1,32 @@
# frozen_string_literal: true
class LooseForeignKeys::DeletedRecord < ApplicationRecord
- extend SuppressCompositePrimaryKeyWarning
+ self.primary_key = :id
+
+ scope :for_table, -> (table) { where(fully_qualified_table_name: table) }
+ scope :consume_order, -> { order(:partition, :consume_after, :id) }
+
+ enum status: { pending: 1, processed: 2 }, _prefix: :status
+
+ def self.load_batch_for_table(table, batch_size)
+ for_table(table)
+ .status_pending
+ .consume_order
+ .limit(batch_size)
+ .to_a
+ end
+
+ def self.mark_records_processed(all_records)
+ # Run a query for each partition to optimize the row lookup by primary key (partition, id)
+ update_count = 0
+
+ all_records.group_by(&:partition).each do |partition, records_within_partition|
+ update_count += status_pending
+ .where(partition: partition)
+ .where(id: records_within_partition.pluck(:id))
+ .update_all(status: :processed)
+ end
+
+ update_count
+ end
end
diff --git a/app/models/loose_foreign_keys/modification_tracker.rb b/app/models/loose_foreign_keys/modification_tracker.rb
new file mode 100644
index 00000000000..6eb04608cd9
--- /dev/null
+++ b/app/models/loose_foreign_keys/modification_tracker.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module LooseForeignKeys
+ class ModificationTracker
+ MAX_DELETES = 100_000
+ MAX_UPDATES = 50_000
+ MAX_RUNTIME = 3.minutes
+
+ delegate :monotonic_time, to: :'Gitlab::Metrics::System'
+
+ def initialize
+ @delete_count_by_table = Hash.new { |h, k| h[k] = 0 }
+ @update_count_by_table = Hash.new { |h, k| h[k] = 0 }
+ @start_time = monotonic_time
+ @deletes_counter = Gitlab::Metrics.counter(
+ :loose_foreign_key_deletions,
+ 'The number of loose foreign key deletions'
+ )
+ @updates_counter = Gitlab::Metrics.counter(
+ :loose_foreign_key_updates,
+ 'The number of loose foreign key updates'
+ )
+ end
+
+ def add_deletions(table, count)
+ @delete_count_by_table[table] += count
+ @deletes_counter.increment({ table: table }, count)
+ end
+
+ def add_updates(table, count)
+ @update_count_by_table[table] += count
+ @updates_counter.increment({ table: table }, count)
+ end
+
+ def over_limit?
+ @delete_count_by_table.values.sum >= MAX_DELETES ||
+ @update_count_by_table.values.sum >= MAX_UPDATES ||
+ monotonic_time - @start_time >= MAX_RUNTIME
+ end
+
+ def stats
+ {
+ over_limit: over_limit?,
+ delete_count_by_table: @delete_count_by_table,
+ update_count_by_table: @update_count_by_table,
+ delete_count: @delete_count_by_table.values.sum,
+ update_count: @update_count_by_table.values.sum
+ }
+ end
+ end
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index 21fd4aebd7b..11f67a77ee2 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -13,6 +13,7 @@ class Member < ApplicationRecord
include FromUnion
include UpdateHighestRole
include RestrictedSignup
+ include Gitlab::Experiment::Dsl
AVATAR_SIZE = 40
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
@@ -22,8 +23,10 @@ class Member < ApplicationRecord
belongs_to :created_by, class_name: "User"
belongs_to :user
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+ has_one :member_task
delegate :name, :username, :email, to: :user, prefix: true
+ delegate :tasks_to_be_done, to: :member_task, allow_nil: true
validates :expires_at, allow_blank: true, future_date: true
validates :user, presence: true, unless: :invite?
@@ -413,6 +416,14 @@ class Member < ApplicationRecord
def after_accept_invite
post_create_hook
+
+ if experiment(:invite_members_for_task).enabled?
+ run_after_commit_or_now do
+ if member_task
+ TasksToBeDone::CreateWorker.perform_async(member_task.id, created_by_id, [user_id.to_i])
+ end
+ end
+ end
end
def after_decline_invite
diff --git a/app/models/members/member_task.rb b/app/models/members/member_task.rb
new file mode 100644
index 00000000000..f093619ff36
--- /dev/null
+++ b/app/models/members/member_task.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class MemberTask < ApplicationRecord
+ TASKS = {
+ code: 0,
+ ci: 1,
+ issues: 2
+ }.freeze
+
+ belongs_to :member
+ belongs_to :project
+
+ validates :member, :project, presence: true
+ validates :tasks, inclusion: { in: TASKS.values }
+ validate :tasks_uniqueness
+ validate :project_in_member_source
+
+ scope :for_members, -> (members) { joins(:member).where(member: members) }
+
+ def tasks_to_be_done
+ Array(self[:tasks]).map { |task| TASKS.key(task) }
+ end
+
+ def tasks_to_be_done=(tasks)
+ self[:tasks] = Array(tasks).map do |task|
+ TASKS[task.to_sym]
+ end.uniq
+ end
+
+ private
+
+ def tasks_uniqueness
+ errors.add(:tasks, 'are not unique') unless Array(tasks).length == Array(tasks).uniq.length
+ end
+
+ def project_in_member_source
+ if member.is_a?(GroupMember)
+ errors.add(:project, _('is not in the member group')) unless project.namespace == member.source
+ elsif member.is_a?(ProjectMember)
+ errors.add(:project, _('is not the member project')) unless project == member.source
+ end
+ end
+end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index eec46b3493e..89b72508e84 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -94,7 +94,6 @@ class ProjectMember < Member
override :refresh_member_authorized_projects
def refresh_member_authorized_projects(blocking:)
- return super unless Feature.enabled?(:specialized_service_for_project_member_auth_refresh)
return unless user
# rubocop:disable CodeReuse/ServiceClass
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 15862fb2bfa..0cd8f12088c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -268,7 +268,6 @@ class MergeRequest < ApplicationRecord
from_fork.where('source_project_id = ? OR target_project_id = ?', project.id, project.id)
end
scope :merged, -> { with_state(:merged) }
- scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :open_and_closed, -> { with_states(:opened, :closed) }
scope :drafts, -> { where(draft: true) }
scope :from_source_branches, ->(branches) { where(source_branch: branches) }
@@ -663,7 +662,7 @@ class MergeRequest < ApplicationRecord
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
def merge_async(user_id, params)
- jid = MergeWorker.perform_async(id, user_id, params.to_h)
+ jid = MergeWorker.with_status.perform_async(id, user_id, params.to_h)
update_column(:merge_jid, jid)
# merge_ongoing? depends on merge_jid
@@ -682,7 +681,7 @@ class MergeRequest < ApplicationRecord
# attribute is set *and* that the sidekiq job is still running. So a JID
# for a completed RebaseWorker is equivalent to a nil JID.
jid = Sidekiq::Worker.skipping_transaction_check do
- RebaseWorker.perform_async(id, user_id, skip_ci)
+ RebaseWorker.with_status.perform_async(id, user_id, skip_ci)
end
update_column(:rebase_jid, jid)
@@ -1317,6 +1316,10 @@ class MergeRequest < ApplicationRecord
end
def default_merge_commit_message(include_description: false)
+ if self.target_project.merge_commit_template.present? && !include_description
+ return ::Gitlab::MergeRequests::MergeCommitMessage.new(merge_request: self).message
+ end
+
closes_issues_references = visible_closing_issues_for.map do |issue|
issue.to_reference(target_project)
end
@@ -1409,7 +1412,15 @@ class MergeRequest < ApplicationRecord
def environments
return Environment.none unless actual_head_pipeline&.merge_request?
- actual_head_pipeline.environments
+ build_for_actual_head_pipeline = Ci::Build.latest.where(pipeline: actual_head_pipeline)
+
+ environments = build_for_actual_head_pipeline.joins(:metadata)
+ .where.not('ci_builds_metadata.expanded_environment_name' => nil)
+ .distinct('ci_builds_metadata.expanded_environment_name')
+ .limit(100)
+ .pluck(:expanded_environment_name)
+
+ Environment.where(project: project, name: environments)
end
def fetch_ref!
@@ -1907,6 +1918,10 @@ class MergeRequest < ApplicationRecord
true
end
+ def find_assignee(user)
+ merge_request_assignees.find_by(user_id: user.id)
+ end
+
def find_reviewer(user)
merge_request_reviewers.find_by(user_id: user.id)
end
@@ -1930,6 +1945,10 @@ class MergeRequest < ApplicationRecord
end
end
+ def attention_requested_enabled?
+ Feature.enabled?(:mr_attention_requests, project, default_enabled: :yaml)
+ end
+
private
def set_draft_status
diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb
index 86bf950ae19..fd8e5860040 100644
--- a/app/models/merge_request_assignee.rb
+++ b/app/models/merge_request_assignee.rb
@@ -1,10 +1,16 @@
# frozen_string_literal: true
class MergeRequestAssignee < ApplicationRecord
+ include MergeRequestReviewerState
+
belongs_to :merge_request, touch: true
belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :merge_request_assignees
validates :assignee, uniqueness: { scope: :merge_request_id }
scope :in_projects, ->(project_ids) { joins(:merge_request).where(merge_requests: { target_project_id: project_ids }) }
+
+ def cache_key
+ [model_name.cache_key, id, state, assignee.cache_key]
+ end
end
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index 09824ed4468..ebbdecf8aa7 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -26,7 +26,7 @@ class MergeRequestContextCommit < ApplicationRecord
# create MergeRequestContextCommit by given commit sha and it's diff file record
def self.bulk_insert(rows, **args)
- Gitlab::Database.main.bulk_insert('merge_request_context_commits', rows, **args) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert('merge_request_context_commits', rows, **args) # rubocop:disable Gitlab/BulkInsert
end
def to_commit
diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb
index b9efebe3af2..fdf57068928 100644
--- a/app/models/merge_request_context_commit_diff_file.rb
+++ b/app/models/merge_request_context_commit_diff_file.rb
@@ -14,7 +14,7 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord
# create MergeRequestContextCommitDiffFile by given diff file record(s)
def self.bulk_insert(*args)
- Gitlab::Database.main.bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert
end
def path
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index bd94c0ad30e..2516ff05bda 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -515,7 +515,7 @@ class MergeRequestDiff < ApplicationRecord
transaction do
MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all
- Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
save!
end
@@ -535,7 +535,7 @@ class MergeRequestDiff < ApplicationRecord
transaction do
MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all
- Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
update!(stored_externally: false)
end
@@ -595,7 +595,7 @@ class MergeRequestDiff < ApplicationRecord
rows = build_external_merge_request_diff_files(rows) if use_external_diff?
# Faster inserts
- Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
end
def build_external_diff_tempfile(rows)
@@ -710,7 +710,7 @@ class MergeRequestDiff < ApplicationRecord
end
CommitCollection
- .new(merge_request.source_project, commits, merge_request.source_branch)
+ .new(merge_request.target_project, commits, merge_request.target_branch)
end
def save_diffs
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index d9a1784cdda..66f1e45fd49 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -6,6 +6,12 @@ class MergeRequestDiffCommit < ApplicationRecord
include BulkInsertSafe
include ShaAttribute
include CachedCommit
+ include IgnorableColumns
+ include FromUnion
+
+ ignore_column %i[author_name author_email committer_name committer_email],
+ remove_with: '14.6',
+ remove_after: '2021-11-22'
belongs_to :merge_request_diff
@@ -51,9 +57,14 @@ class MergeRequestDiffCommit < ApplicationRecord
committer =
users[[commit_hash[:committer_name], commit_hash[:committer_email]]]
+ # These fields are only used to determine the author/committer IDs, we
+ # don't store them in the DB.
+ commit_hash = commit_hash
+ .except(:author_name, :author_email, :committer_name, :committer_email)
+
commit_hash.merge(
- commit_author_id: author&.id,
- committer_id: committer&.id,
+ commit_author_id: author.id,
+ committer_id: committer.id,
merge_request_diff_id: merge_request_diff_id,
relative_order: index,
sha: Gitlab::Database::ShaAttribute.serialize(sha), # rubocop:disable Cop/ActiveRecordSerialize
@@ -63,7 +74,7 @@ class MergeRequestDiffCommit < ApplicationRecord
)
end
- Gitlab::Database.main.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
end
def self.prepare_commits_for_bulk_insert(commits)
@@ -104,18 +115,18 @@ class MergeRequestDiffCommit < ApplicationRecord
end
def author_name
- commit_author_id ? commit_author.name : super
+ commit_author&.name
end
def author_email
- commit_author_id ? commit_author.email : super
+ commit_author&.email
end
def committer_name
- committer_id ? committer.name : super
+ committer&.name
end
def committer_email
- committer_id ? committer.email : super
+ committer&.email
end
end
diff --git a/app/models/merge_request_reviewer.rb b/app/models/merge_request_reviewer.rb
index 4a1f31a7f39..4abf0fa09f0 100644
--- a/app/models/merge_request_reviewer.rb
+++ b/app/models/merge_request_reviewer.rb
@@ -1,15 +1,12 @@
# frozen_string_literal: true
class MergeRequestReviewer < ApplicationRecord
- enum state: {
- unreviewed: 0,
- reviewed: 1
- }
-
- validates :state,
- presence: true,
- inclusion: { in: MergeRequestReviewer.states.keys }
+ include MergeRequestReviewerState
belongs_to :merge_request
belongs_to :reviewer, class_name: 'User', foreign_key: :user_id, inverse_of: :merge_request_reviewers
+
+ def cache_key
+ [model_name.cache_key, id, state, reviewer.cache_key]
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 07f9bb99952..353a896b3fe 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -16,9 +16,11 @@ class Namespace < ApplicationRecord
include Namespaces::Traversal::Linear
include EachBatch
- ignore_column :delayed_project_removal, remove_with: '14.1', remove_after: '2021-05-22'
+ # Temporary column used for back-filling project namespaces.
+ # Remove it once the back-filling of all project namespaces is done.
+ ignore_column :tmp_project_id, remove_with: '14.7', remove_after: '2022-01-22'
- # Tells ActiveRecord not to store the full class name, in order to space some space
+ # Tells ActiveRecord not to store the full class name, in order to save some space
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69794
self.store_full_sti_class = false
self.store_full_class_name = false
@@ -54,7 +56,7 @@ class Namespace < ApplicationRecord
belongs_to :owner, class_name: "User"
belongs_to :parent, class_name: "Namespace"
- has_many :children, class_name: "Namespace", foreign_key: :parent_id
+ has_many :children, -> { where(type: Group.sti_name) }, class_name: "Namespace", foreign_key: :parent_id
has_many :custom_emoji, inverse_of: :namespace
has_one :chat_team, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :root_storage_statistics, class_name: 'Namespace::RootStorageStatistics'
@@ -95,9 +97,11 @@ class Namespace < ApplicationRecord
validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
validate :validate_parent_type, if: -> { Feature.enabled?(:validate_namespace_parent_type, default_enabled: :yaml) }
- validate :nesting_level_allowed
- validate :changing_shared_runners_enabled_is_allowed
- validate :changing_allow_descendants_override_disabled_shared_runners_is_allowed
+
+ # ProjectNamespaces excluded as they are not meant to appear in the group hierarchy at the moment.
+ validate :nesting_level_allowed, unless: -> { project_namespace? }
+ validate :changing_shared_runners_enabled_is_allowed, unless: -> { project_namespace? }
+ validate :changing_allow_descendants_override_disabled_shared_runners_is_allowed, unless: -> { project_namespace? }
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :avatar_url, to: :owner, allow_nil: true
@@ -123,7 +127,7 @@ class Namespace < ApplicationRecord
scope :user_namespaces, -> { where(type: [nil, Namespaces::UserNamespace.sti_name]) }
# TODO: this can be simplified with `type != 'Project'` when working on issue
# https://gitlab.com/gitlab-org/gitlab/-/issues/341070
- scope :without_project_namespaces, -> { where("type IS DISTINCT FROM ?", Namespaces::ProjectNamespace.sti_name) }
+ scope :without_project_namespaces, -> { where(Namespace.arel_table[:type].is_distinct_from(Namespaces::ProjectNamespace.sti_name)) }
scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
scope :include_route, -> { includes(:route) }
scope :by_parent, -> (parent) { where(parent_id: parent) }
@@ -140,6 +144,7 @@ class Namespace < ApplicationRecord
'COALESCE(SUM(ps.snippets_size), 0) AS snippets_size',
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
+ 'COALESCE(SUM(ps.pipeline_artifacts_size), 0) AS pipeline_artifacts_size',
'COALESCE(SUM(ps.packages_size), 0) AS packages_size',
'COALESCE(SUM(ps.uploads_size), 0) AS uploads_size'
)
@@ -189,9 +194,9 @@ class Namespace < ApplicationRecord
# Returns an ActiveRecord::Relation.
def search(query, include_parents: false)
if include_parents
- where(id: Route.for_routable_type(Namespace.name).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]]).select(:source_id))
+ without_project_namespaces.where(id: Route.for_routable_type(Namespace.name).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]]).select(:source_id))
else
- fuzzy_search(query, [:path, :name])
+ without_project_namespaces.fuzzy_search(query, [:path, :name])
end
end
@@ -494,6 +499,10 @@ class Namespace < ApplicationRecord
Feature.enabled?(:block_issue_repositioning, self, type: :ops, default_enabled: :yaml)
end
+ def project_namespace_creation_enabled?
+ Feature.enabled?(:create_project_namespace_on_project_create, self, default_enabled: :yaml)
+ end
+
private
def expire_child_caches
@@ -535,21 +544,23 @@ class Namespace < ApplicationRecord
# Until we compare the inconsistency rates of the new specialized worker and
# the old approach, we still run AuthorizedProjectsWorker
# but with some delay and lower urgency as a safety net.
- Group
- .joins(project_group_links: :project)
- .where(projects: { namespace_id: id })
- .distinct
- .find_each do |group|
- group.refresh_members_authorized_projects(
- blocking: false,
- priority: UserProjectAccessChangedService::LOW_PRIORITY
- )
- end
+ enqueue_jobs_for_groups_requiring_authorizations_refresh(priority: UserProjectAccessChangedService::LOW_PRIORITY)
else
- Group
- .joins(project_group_links: :project)
- .where(projects: { namespace_id: id })
- .find_each(&:refresh_members_authorized_projects)
+ enqueue_jobs_for_groups_requiring_authorizations_refresh(priority: UserProjectAccessChangedService::HIGH_PRIORITY)
+ end
+ end
+
+ def enqueue_jobs_for_groups_requiring_authorizations_refresh(priority:)
+ groups_requiring_authorizations_refresh = Group
+ .joins(project_group_links: :project)
+ .where(projects: { namespace_id: id })
+ .distinct
+
+ groups_requiring_authorizations_refresh.find_each do |group|
+ group.refresh_members_authorized_projects(
+ blocking: false,
+ priority: priority
+ )
end
end
@@ -573,7 +584,7 @@ class Namespace < ApplicationRecord
end
if user_namespace?
- errors.add(:parent_id, _('cannot not be used for user namespace'))
+ errors.add(:parent_id, _('cannot be used for user namespace'))
elsif group_namespace?
errors.add(:parent_id, _('user namespace cannot be the parent of another namespace')) if parent.user_namespace?
end
diff --git a/app/models/namespaces/project_namespace.rb b/app/models/namespaces/project_namespace.rb
index d1806c1c088..22ec550dee2 100644
--- a/app/models/namespaces/project_namespace.rb
+++ b/app/models/namespaces/project_namespace.rb
@@ -4,8 +4,6 @@ module Namespaces
class ProjectNamespace < Namespace
has_one :project, foreign_key: :project_namespace_id, inverse_of: :project_namespace
- validates :project, presence: true
-
def self.sti_name
'Project'
end
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index d7130322ed1..1736fe82ca5 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -161,7 +161,7 @@ module Namespaces
def lineage(top: nil, bottom: nil, hierarchy_order: nil)
raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom
- skope = self.class.without_sti_condition
+ skope = self.class
if top
skope = skope.where("traversal_ids @> ('{?}')", top.id)
@@ -181,7 +181,6 @@ module Namespaces
# standard SELECT to avoid mismatched attribute errors when trying to
# chain future ActiveRelation commands, and retain the ordering.
skope = self.class
- .without_sti_condition
.from(skope, self.class.table_name)
.select(skope.arel_table[Arel.star])
.order(depth: hierarchy_order)
diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb
index 2da0e48c2da..f5c44171c42 100644
--- a/app/models/namespaces/traversal/linear_scopes.rb
+++ b/app/models/namespaces/traversal/linear_scopes.rb
@@ -15,12 +15,18 @@ module Namespaces
select('namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id')
end
+ def roots
+ return super unless use_traversal_ids_roots?
+
+ root_ids = all.select("#{quoted_table_name}.traversal_ids[1]").distinct
+ unscoped.where(id: root_ids)
+ end
+
def self_and_ancestors(include_self: true, hierarchy_order: nil)
return super unless use_traversal_ids_for_ancestor_scopes?
records = unscoped
- .without_sti_condition
- .where(id: without_sti_condition.select('unnest(traversal_ids)'))
+ .where(id: select('unnest(traversal_ids)'))
.order_by_depth(hierarchy_order)
.normal_select
@@ -40,24 +46,24 @@ module Namespaces
def self_and_descendants(include_self: true)
return super unless use_traversal_ids?
- records = self_and_descendants_with_duplicates(include_self: include_self)
-
- distinct = records.select('DISTINCT on(namespaces.id) namespaces.*')
-
- distinct.normal_select
+ if Feature.enabled?(:traversal_ids_btree, default_enabled: :yaml)
+ self_and_descendants_with_comparison_operators(include_self: include_self)
+ else
+ records = self_and_descendants_with_duplicates_with_array_operator(include_self: include_self)
+ distinct = records.select('DISTINCT on(namespaces.id) namespaces.*')
+ distinct.normal_select
+ end
end
def self_and_descendant_ids(include_self: true)
return super unless use_traversal_ids?
- self_and_descendants_with_duplicates(include_self: include_self)
- .select('DISTINCT namespaces.id')
- end
-
- # Make sure we drop the STI `type = 'Group'` condition for better performance.
- # Logically equivalent so long as hierarchies remain homogeneous.
- def without_sti_condition
- unscope(where: :type)
+ if Feature.enabled?(:traversal_ids_btree, default_enabled: :yaml)
+ self_and_descendants_with_comparison_operators(include_self: include_self).as_ids
+ else
+ self_and_descendants_with_duplicates_with_array_operator(include_self: include_self)
+ .select('DISTINCT namespaces.id')
+ end
end
def order_by_depth(hierarchy_order)
@@ -75,7 +81,7 @@ module Namespaces
# When we have queries that break this SELECT * format we can run in to errors.
# For example `SELECT DISTINCT on(...)` will fail when we chain a `.count` c
def normal_select
- unscoped.without_sti_condition.from(all, :namespaces)
+ unscoped.from(all, :namespaces)
end
private
@@ -84,16 +90,52 @@ module Namespaces
Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
end
+ def use_traversal_ids_roots?
+ Feature.enabled?(:use_traversal_ids_roots, default_enabled: :yaml) &&
+ use_traversal_ids?
+ end
+
def use_traversal_ids_for_ancestor_scopes?
Feature.enabled?(:use_traversal_ids_for_ancestor_scopes, default_enabled: :yaml) &&
use_traversal_ids?
end
- def self_and_descendants_with_duplicates(include_self: true)
+ def self_and_descendants_with_comparison_operators(include_self: true)
+ base = all.select(
+ :traversal_ids,
+ 'LEAD (namespaces.traversal_ids, 1) OVER (ORDER BY namespaces.traversal_ids ASC) next_traversal_ids'
+ )
+ cte = Gitlab::SQL::CTE.new(:base_cte, base)
+
+ namespaces = Arel::Table.new(:namespaces)
+ records = unscoped
+ .with(cte.to_arel)
+ .from([cte.table, namespaces])
+
+ # Bound the search space to ourselves (optional) and descendants.
+ #
+ # WHERE (base_cte.next_traversal_ids IS NULL OR base_cte.next_traversal_ids > namespaces.traversal_ids)
+ # AND next_traversal_ids_sibling(base_cte.traversal_ids) > namespaces.traversal_ids
+ records = records
+ .where(cte.table[:next_traversal_ids].eq(nil).or(cte.table[:next_traversal_ids].gt(namespaces[:traversal_ids])))
+ .where(next_sibling_func(cte.table[:traversal_ids]).gt(namespaces[:traversal_ids]))
+
+ # AND base_cte.traversal_ids <= namespaces.traversal_ids
+ if include_self
+ records.where(cte.table[:traversal_ids].lteq(namespaces[:traversal_ids]))
+ else
+ records.where(cte.table[:traversal_ids].lt(namespaces[:traversal_ids]))
+ end
+ end
+
+ def next_sibling_func(*args)
+ Arel::Nodes::NamedFunction.new('next_traversal_ids_sibling', args)
+ end
+
+ def self_and_descendants_with_duplicates_with_array_operator(include_self: true)
base_ids = select(:id)
records = unscoped
- .without_sti_condition
.from("namespaces, (#{base_ids.to_sql}) base")
.where('namespaces.traversal_ids @> ARRAY[base.id]')
diff --git a/app/models/namespaces/traversal/recursive_scopes.rb b/app/models/namespaces/traversal/recursive_scopes.rb
index 6659cefe095..925d9b8bb0c 100644
--- a/app/models/namespaces/traversal/recursive_scopes.rb
+++ b/app/models/namespaces/traversal/recursive_scopes.rb
@@ -10,6 +10,13 @@ module Namespaces
select('id')
end
+ def roots
+ Gitlab::ObjectHierarchy
+ .new(all)
+ .base_and_ancestors
+ .where(namespaces: { parent_id: nil })
+ end
+
def self_and_ancestors(include_self: true, hierarchy_order: nil)
records = Gitlab::ObjectHierarchy.new(all).base_and_ancestors(hierarchy_order: hierarchy_order)
diff --git a/app/models/namespaces/user_namespace.rb b/app/models/namespaces/user_namespace.rb
index 22b7a0a3b2b..d4d7d352e71 100644
--- a/app/models/namespaces/user_namespace.rb
+++ b/app/models/namespaces/user_namespace.rb
@@ -3,6 +3,26 @@
# TODO: currently not created/mapped in the database, will be done in another issue
# https://gitlab.com/gitlab-org/gitlab/-/issues/341070
module Namespaces
+ ####################################################################
+ # PLEASE DO NOT OVERRIDE METHODS IN THIS CLASS!
+ #
+ # This class is a placeholder for STI. But we also want to ensure
+ # tests using `:namespace` factory are still testing the same functionality.
+ #
+ # Many legacy tests use `:namespace` which has a slight semantic
+ # mismatch as it always has been a User (personal) namespace.
+ #
+ # If you need to make a change here, please ping the
+ # Manage/Workspaces group so we can ensure that the
+ # changes do not break existing functionality.
+ #
+ # As Namespaces evolve we may be able to relax this restriction
+ # but for now, please check in with us <3
+ #
+ # For details, see the discussion in
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74152
+ ####################################################################
+
class UserNamespace < Namespace
def self.sti_name
'User'
diff --git a/app/models/note.rb b/app/models/note.rb
index 37473518892..cb285028203 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -114,6 +114,7 @@ class Note < ApplicationRecord
scope :fresh, -> { order_created_asc.with_order_id_asc }
scope :updated_after, ->(time) { where('updated_at > ?', time) }
scope :with_updated_at, ->(time) { where(updated_at: time) }
+ scope :with_discussion_ids, ->(discussion_ids) { where(discussion_id: discussion_ids) }
scope :with_suggestions, -> { joins(:suggestions) }
scope :inc_author, -> { includes(:author) }
scope :with_api_entity_associations, -> { preload(:note_diff_file, :author) }
diff --git a/app/models/packages/npm.rb b/app/models/packages/npm.rb
index e49199d911c..9221187d92a 100644
--- a/app/models/packages/npm.rb
+++ b/app/models/packages/npm.rb
@@ -9,5 +9,9 @@ module Packages
package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first
end
+
+ def self.table_name_prefix
+ 'packages_npm_'
+ end
end
end
diff --git a/app/models/packages/npm/metadatum.rb b/app/models/packages/npm/metadatum.rb
new file mode 100644
index 00000000000..7388c4bdbd2
--- /dev/null
+++ b/app/models/packages/npm/metadatum.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Packages::Npm::Metadatum < ApplicationRecord
+ belongs_to :package, -> { where(package_type: :npm) }, inverse_of: :npm_metadatum
+
+ validates :package, presence: true
+ # From https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
+ validates :package_json, json_schema: { filename: "npm_package_json" }
+ validate :ensure_npm_package_type
+ validate :ensure_package_json_size
+
+ private
+
+ def ensure_npm_package_type
+ return if package&.npm?
+
+ errors.add(:base, _('Package type must be NPM'))
+ end
+
+ def ensure_package_json_size
+ return if package_json.to_s.size < 20000
+
+ errors.add(:package_json, _('structure is too large'))
+ end
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 34eae6ab5dc..962a1057a22 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -39,8 +39,9 @@ class Packages::Package < ApplicationRecord
has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum'
has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum'
has_one :rubygems_metadatum, inverse_of: :package, class_name: 'Packages::Rubygems::Metadatum'
+ has_one :npm_metadatum, inverse_of: :package, class_name: 'Packages::Npm::Metadatum'
has_many :build_infos, inverse_of: :package
- has_many :pipelines, through: :build_infos
+ has_many :pipelines, through: :build_infos, disable_joins: true
has_one :debian_publication, inverse_of: :package, class_name: 'Packages::Debian::Publication'
has_one :debian_distribution, through: :debian_publication, source: :distribution, inverse_of: :packages, class_name: 'Packages::Debian::ProjectDistribution'
@@ -102,7 +103,6 @@ class Packages::Package < ApplicationRecord
scope :with_status, ->(status) { where(status: status) }
scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) }
scope :installable, -> { with_status(INSTALLABLE_STATUSES) }
- scope :including_build_info, -> { includes(pipelines: :user) }
scope :including_project_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) }
scope :including_dependency_links, -> { includes(dependency_links: :dependency) }
@@ -126,11 +126,13 @@ class Packages::Package < ApplicationRecord
.where(Packages::Composer::Metadatum.table_name => { target_sha: target })
end
scope :preload_composer, -> { preload(:composer_metadatum) }
+ scope :preload_npm_metadatum, -> { preload(:npm_metadatum) }
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
scope :has_version, -> { where.not(version: nil) }
scope :preload_files, -> { preload(:package_files) }
+ scope :preload_pipelines, -> { preload(pipelines: :user) }
scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) }
scope :limit_recent, ->(limit) { order_created_desc.limit(limit) }
scope :select_distinct_name, -> { select(:name).distinct }
@@ -245,7 +247,7 @@ class Packages::Package < ApplicationRecord
def versions
project.packages
- .including_build_info
+ .preload_pipelines
.including_tags
.with_name(name)
.where.not(version: version)
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index 14701b8a800..87c9f56cc41 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -15,7 +15,7 @@ class Packages::PackageFile < ApplicationRecord
has_one :conan_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Conan::FileMetadatum'
has_many :package_file_build_infos, inverse_of: :package_file, class_name: 'Packages::PackageFileBuildInfo'
- has_many :pipelines, through: :package_file_build_infos
+ has_many :pipelines, through: :package_file_build_infos, disable_joins: true
has_one :debian_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Debian::FileMetadatum'
has_one :helm_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Helm::FileMetadatum'
@@ -38,6 +38,7 @@ class Packages::PackageFile < ApplicationRecord
scope :with_format, ->(format) { where(::Packages::PackageFile.arel_table[:file_name].matches("%.#{format}")) }
scope :preload_package, -> { preload(:package) }
+ scope :preload_pipelines, -> { preload(pipelines: :user) }
scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) }
scope :preload_debian_file_metadata, -> { preload(:debian_file_metadatum) }
scope :preload_helm_file_metadata, -> { preload(:helm_file_metadatum) }
diff --git a/app/models/preloaders/group_policy_preloader.rb b/app/models/preloaders/group_policy_preloader.rb
new file mode 100644
index 00000000000..95d6e0b5c1f
--- /dev/null
+++ b/app/models/preloaders/group_policy_preloader.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Preloaders
+ class GroupPolicyPreloader
+ def initialize(groups, current_user)
+ @groups = groups
+ @current_user = current_user
+ end
+
+ def execute
+ Preloaders::UserMaxAccessLevelInGroupsPreloader.new(@groups, @current_user).execute
+ Preloaders::GroupRootAncestorPreloader.new(@groups, root_ancestor_preloads).execute
+ end
+
+ private
+
+ def root_ancestor_preloads
+ []
+ end
+ end
+end
+
+Preloaders::GroupPolicyPreloader.prepend_mod_with('Preloaders::GroupPolicyPreloader')
diff --git a/app/models/preloaders/group_root_ancestor_preloader.rb b/app/models/preloaders/group_root_ancestor_preloader.rb
new file mode 100644
index 00000000000..3ca713d9635
--- /dev/null
+++ b/app/models/preloaders/group_root_ancestor_preloader.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Preloaders
+ class GroupRootAncestorPreloader
+ def initialize(groups, root_ancestor_preloads = [])
+ @groups = groups
+ @root_ancestor_preloads = root_ancestor_preloads
+ end
+
+ def execute
+ return unless ::Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
+
+ # type == 'Group' condition located on subquery to prevent a filter in the query
+ root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
+ .select('namespaces.*, root_query.id as source_id')
+
+ root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
+
+ root_ancestors_by_id = root_query.group_by(&:source_id)
+
+ @groups.each do |group|
+ group.root_ancestor = root_ancestors_by_id[group.id].first
+ end
+ end
+
+ private
+
+ def join_sql
+ Group.select('id, traversal_ids[1] as root_id').where(id: @groups.map(&:id)).to_sql
+ end
+ end
+end
diff --git a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
index 14f1d271572..bdd76d39ec1 100644
--- a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
+++ b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
@@ -3,7 +3,6 @@
module Preloaders
# This class preloads the max access level (role) for the user within the given groups and
# stores the values in requests store.
- # Will only be able to preload max access level for groups where the user is a direct member
class UserMaxAccessLevelInGroupsPreloader
include BulkMemberAccessLoad
@@ -13,8 +12,17 @@ module Preloaders
end
def execute
+ if ::Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
+ preload_with_traversal_ids
+ else
+ preload_direct_memberships
+ end
+ end
+
+ private
+
+ def preload_direct_memberships
group_memberships = GroupMember.active_without_invites_and_requests
- .non_minimal_access
.where(user: @user, source_id: @groups)
.group(:source_id)
.maximum(:access_level)
@@ -23,5 +31,22 @@ module Preloaders
merge_value_to_request_store(User, @user.id, group_id, max_access_level)
end
end
+
+ def preload_with_traversal_ids
+ max_access_levels = GroupMember.active_without_invites_and_requests
+ .where(user: @user)
+ .joins("INNER JOIN (#{traversal_join_sql}) as hierarchy ON members.source_id = hierarchy.traversal_id")
+ .group('hierarchy.id')
+ .maximum(:access_level)
+
+ @groups.each do |group|
+ max_access_level = max_access_levels[group.id] || Gitlab::Access::NO_ACCESS
+ merge_value_to_request_store(User, @user.id, group.id, max_access_level)
+ end
+ end
+
+ def traversal_join_sql
+ Namespace.select('id, unnest(traversal_ids) as traversal_id').where(id: @groups.map(&:id)).to_sql
+ end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 2ceba10e86e..2288850553c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -19,7 +19,6 @@ class Project < ApplicationRecord
include Presentable
include HasRepository
include HasWiki
- include HasIntegrations
include CanMoveRepositoryStorage
include Routable
include GroupDescendant
@@ -98,7 +97,7 @@ class Project < ApplicationRecord
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
before_save :ensure_runners_token
- before_save :ensure_project_namespace_in_sync
+ before_validation :ensure_project_namespace_in_sync
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
@@ -147,7 +146,7 @@ class Project < ApplicationRecord
belongs_to :namespace
# Sync deletion via DB Trigger to ensure we do not have
# a project without a project_namespace (or vice-versa)
- belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id', inverse_of: :project
+ belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id'
alias_method :parent, :namespace
alias_attribute :parent_id, :namespace_id
@@ -189,6 +188,7 @@ class Project < ApplicationRecord
has_one :prometheus_integration, class_name: 'Integrations::Prometheus', inverse_of: :project
has_one :pushover_integration, class_name: 'Integrations::Pushover'
has_one :redmine_integration, class_name: 'Integrations::Redmine'
+ has_one :shimo_integration, class_name: 'Integrations::Shimo'
has_one :slack_integration, class_name: 'Integrations::Slack'
has_one :slack_slash_commands_integration, class_name: 'Integrations::SlackSlashCommands'
has_one :teamcity_integration, class_name: 'Integrations::Teamcity'
@@ -451,6 +451,7 @@ class Project < ApplicationRecord
:allow_merge_on_skipped_pipeline=, :has_confluence?,
to: :project_setting
delegate :active?, to: :prometheus_integration, allow_nil: true, prefix: true
+ delegate :merge_commit_template, :merge_commit_template=, to: :project_setting, allow_nil: true
delegate :log_jira_dvcs_integration_usage, :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
@@ -475,6 +476,7 @@ class Project < ApplicationRecord
validates :project_feature, presence: true
validates :namespace, presence: true
+ validates :project_namespace, presence: true, if: -> { self.namespace && self.root_namespace.project_namespace_creation_enabled? }
validates :name, uniqueness: { scope: :namespace_id }
validates :import_url, public_url: { schemes: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
@@ -492,6 +494,7 @@ class Project < ApplicationRecord
validates :variables, nested_attributes_duplicates: { scope: :environment_scope }
validates :bfg_object_map, file_size: { maximum: :max_attachment_size }
validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
+ validates :suggestion_commit_message, length: { maximum: 255 }
# Scopes
scope :pending_delete, -> { where(pending_delete: true) }
@@ -857,6 +860,18 @@ class Project < ApplicationRecord
rescue ActionController::RoutingError, URI::InvalidURIError
nil
end
+
+ def without_integration(integration)
+ integrations = Integration
+ .select('1')
+ .where("#{Integration.table_name}.project_id = projects.id")
+ .where(type: integration.type)
+
+ Project
+ .where('NOT EXISTS (?)', integrations)
+ .where(pending_delete: false)
+ .where(archived: false)
+ end
end
def initialize(attributes = nil)
@@ -1453,7 +1468,7 @@ class Project < ApplicationRecord
end
def disabled_integrations
- [:zentao]
+ [:shimo]
end
def find_or_initialize_integration(name)
@@ -1777,10 +1792,12 @@ class Project < ApplicationRecord
def all_runners
Ci::Runner.from_union([runners, group_runners, shared_runners])
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937')
end
def all_available_runners
Ci::Runner.from_union([runners, group_runners, available_shared_runners])
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937')
end
# Once issue 339937 is fixed, please search for all mentioned of
@@ -2051,14 +2068,16 @@ class Project < ApplicationRecord
end
def predefined_variables
- Gitlab::Ci::Variables::Collection.new
- .concat(predefined_ci_server_variables)
- .concat(predefined_project_variables)
- .concat(pages_variables)
- .concat(container_registry_variables)
- .concat(dependency_proxy_variables)
- .concat(auto_devops_variables)
- .concat(api_variables)
+ strong_memoize(:predefined_variables) do
+ Gitlab::Ci::Variables::Collection.new
+ .concat(predefined_ci_server_variables)
+ .concat(predefined_project_variables)
+ .concat(pages_variables)
+ .concat(container_registry_variables)
+ .concat(dependency_proxy_variables)
+ .concat(auto_devops_variables)
+ .concat(api_variables)
+ end
end
def predefined_project_variables
@@ -2579,18 +2598,21 @@ class Project < ApplicationRecord
config = Gitlab.config.incoming_email
wildcard = Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER
- config.address&.gsub(wildcard, "#{full_path_slug}-#{id}-issue-")
+ config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}")
end
def service_desk_custom_address
return unless Gitlab::ServiceDeskEmail.enabled?
- key = service_desk_setting&.project_key
- return unless key.present?
+ key = service_desk_setting&.project_key || default_service_desk_suffix
Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}")
end
+ def default_service_desk_suffix
+ "#{id}-issue-"
+ end
+
def root_namespace
if namespace.has_parent?
namespace.root_ancestor
@@ -2911,12 +2933,28 @@ class Project < ApplicationRecord
end
def ensure_project_namespace_in_sync
- if changes.keys & [:name, :path, :namespace_id, :visibility_level] && project_namespace.present?
- project_namespace.name = name
- project_namespace.path = path
- project_namespace.parent = namespace
- project_namespace.visibility_level = visibility_level
- end
+ # create project_namespace when project is created if create_project_namespace_on_project_create FF is enabled
+ build_project_namespace if project_namespace_creation_enabled?
+
+ # regardless of create_project_namespace_on_project_create FF we need
+ # to keep project and project namespace in sync if there is one
+ sync_attributes(project_namespace) if sync_project_namespace?
+ end
+
+ def project_namespace_creation_enabled?
+ new_record? && !project_namespace && self.namespace && self.root_namespace.project_namespace_creation_enabled?
+ end
+
+ def sync_project_namespace?
+ (changes.keys & %w(name path namespace_id namespace visibility_level shared_runners_enabled)).any? && project_namespace.present?
+ end
+
+ def sync_attributes(project_namespace)
+ project_namespace.name = name
+ project_namespace.path = path
+ project_namespace.parent = namespace
+ project_namespace.shared_runners_enabled = shared_runners_enabled
+ project_namespace.visibility_level = visibility_level
end
end
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 64e768007ee..fed19a37a16 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -24,8 +24,9 @@ class ProjectAuthorization < ApplicationRecord
end
connection.execute <<-EOF.strip_heredoc
- INSERT INTO project_authorizations (user_id, project_id, access_level)
- VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
+ INSERT INTO project_authorizations (user_id, project_id, access_level)
+ VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
+ ON CONFLICT DO NOTHING
EOF
end
end
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 24d892290a6..6c8d2226bc9 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -12,6 +12,8 @@ class ProjectSetting < ApplicationRecord
self.primary_key = :project_id
+ validates :merge_commit_template, length: { maximum: 500 }
+
def squash_enabled_by_default?
%w[always default_on].include?(squash_option)
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 774d81156b7..94904e9792f 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -41,13 +41,15 @@ class ProjectTeam
member
end
- def add_users(users, access_level, current_user: nil, expires_at: nil)
+ def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
Members::Projects::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
current_user: current_user,
- expires_at: expires_at
+ expires_at: expires_at,
+ tasks_to_be_done: tasks_to_be_done,
+ tasks_project_id: tasks_project_id
)
end
diff --git a/app/models/projects/topic.rb b/app/models/projects/topic.rb
index f3352ecc5ee..8d6f8c3a9ca 100644
--- a/app/models/projects/topic.rb
+++ b/app/models/projects/topic.rb
@@ -28,3 +28,5 @@ module Projects
end
end
end
+
+::Projects::Topic.prepend_mod_with('Projects::Topic')
diff --git a/app/models/push_event_payload.rb b/app/models/push_event_payload.rb
index 441b94e1855..8358be35470 100644
--- a/app/models/push_event_payload.rb
+++ b/app/models/push_event_payload.rb
@@ -2,9 +2,6 @@
class PushEventPayload < ApplicationRecord
extend SuppressCompositePrimaryKeyWarning
- include IgnorableColumns
-
- ignore_columns :event_id_convert_to_bigint, remove_with: '14.4', remove_after: '2021-10-22'
include ShaAttribute
diff --git a/app/models/release.rb b/app/models/release.rb
index eac6346cc60..0fda6940249 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -34,6 +34,7 @@ class Release < ApplicationRecord
project: [:project_feature, :route, { namespace: :route }])
}
scope :with_milestones, -> { joins(:milestone_releases) }
+ scope :with_group_milestones, -> { joins(:milestones).where.not(milestones: { group_id: nil }) }
scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) }
scope :without_evidence, -> { left_joins(:evidences).where(::Releases::Evidence.arel_table[:id].eq(nil)) }
scope :released_within_2hrs, -> { where(released_at: Time.zone.now - 1.hour..Time.zone.now + 1.hour) }
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 119d874a6e1..47482f04bca 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -731,10 +731,8 @@ class Repository
raw_repository.local_branches(sort_by: sort_by, pagination_params: pagination_params)
end
- def tags_sorted_by(value)
- return raw_repository.tags(sort_by: value) if Feature.enabled?(:tags_finder_gitaly, project, default_enabled: :yaml)
-
- tags_ruby_sort(value)
+ def tags_sorted_by(value, pagination_params = nil)
+ raw_repository.tags(sort_by: value, pagination_params: pagination_params)
end
# Params:
@@ -1091,6 +1089,13 @@ class Repository
after_create
true
+ rescue Gitlab::Git::Repository::RepositoryExists
+ # We do not want to call `#after_create` given that we didn't create the
+ # repo, but we obviously have a mismatch between what's in our exists cache
+ # and actual on-disk state as seen by Gitaly. Let's thus expire our caches.
+ expire_status_cache
+
+ nil
end
def create_from_bundle(bundle_path)
@@ -1163,34 +1168,6 @@ class Repository
@request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
end
- # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/339741
- def tags_ruby_sort(value)
- case value
- when 'name_asc'
- VersionSorter.sort(tags) { |tag| tag.name }
- when 'name_desc'
- VersionSorter.rsort(tags) { |tag| tag.name }
- when 'updated_desc'
- tags_sorted_by_committed_date.reverse
- when 'updated_asc'
- tags_sorted_by_committed_date
- else
- tags
- end
- end
-
- # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/339741
- def tags_sorted_by_committed_date
- # Annotated tags can point to any object (e.g. a blob), but generally
- # tags point to a commit. If we don't have a commit, then just default
- # to putting the tag at the end of the list.
- default = Time.current
-
- tags.sort_by do |tag|
- tag.dereferenced_target&.committed_date || default
- end
- end
-
def repository_event(event, tags = {})
Gitlab::Metrics.add_event(event, tags)
end
diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb
index ff564d87449..f1ca5c23997 100644
--- a/app/models/suggestion.rb
+++ b/app/models/suggestion.rb
@@ -50,6 +50,7 @@ class Suggestion < ApplicationRecord
next _("Can't apply as the source branch was deleted.") unless noteable.source_branch_exists?
next outdated_reason if outdated?(cached: cached) || !note.active?
next _("This suggestion already matches its content.") unless different_content?
+ next _("This file was modified for readability, and can't accept suggestions. Edit it directly.") if file_path.end_with? "ipynb"
end
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 94a99603848..742b8fd2a9d 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -18,6 +18,7 @@ class Todo < ApplicationRecord
DIRECTLY_ADDRESSED = 7
MERGE_TRAIN_REMOVED = 8 # This is an EE-only feature
REVIEW_REQUESTED = 9
+ ATTENTION_REQUESTED = 10
ACTION_NAMES = {
ASSIGNED => :assigned,
@@ -28,7 +29,8 @@ class Todo < ApplicationRecord
APPROVAL_REQUIRED => :approval_required,
UNMERGEABLE => :unmergeable,
DIRECTLY_ADDRESSED => :directly_addressed,
- MERGE_TRAIN_REMOVED => :merge_train_removed
+ MERGE_TRAIN_REMOVED => :merge_train_removed,
+ ATTENTION_REQUESTED => :attention_requested
}.freeze
belongs_to :author, class_name: "User"
@@ -189,6 +191,10 @@ class Todo < ApplicationRecord
action == REVIEW_REQUESTED
end
+ def attention_requested?
+ action == ATTENTION_REQUESTED
+ end
+
def merge_train_removed?
action == MERGE_TRAIN_REMOVED
end
diff --git a/app/models/upload.rb b/app/models/upload.rb
index c1a3df82457..ac7ebb31abc 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -2,6 +2,7 @@
class Upload < ApplicationRecord
include Checksummable
+
# Upper limit for foreground checksum processing
CHECKSUM_THRESHOLD = 100.megabytes
@@ -51,9 +52,9 @@ class Upload < ApplicationRecord
##
# FastDestroyAll concerns
- def finalize_fast_destroy(keys)
- keys.each do |store_class, paths|
- store_class.new.delete_keys_async(paths)
+ def finalize_fast_destroy(items_to_remove)
+ items_to_remove.each do |store_class, keys|
+ store_class.new.delete_keys_async(keys)
end
end
end
@@ -65,6 +66,10 @@ class Upload < ApplicationRecord
uploader_class.absolute_path(self)
end
+ def relative_path
+ uploader_class.relative_path(self)
+ end
+
def calculate_checksum!
self.checksum = nil
return unless needs_checksum?
diff --git a/app/models/uploads/fog.rb b/app/models/uploads/fog.rb
index b44e273e9ab..5d57b644dbe 100644
--- a/app/models/uploads/fog.rb
+++ b/app/models/uploads/fog.rb
@@ -15,13 +15,21 @@ module Uploads
end
def delete_keys(keys)
- keys.each do |key|
- connection.delete_object(bucket_name, key)
- end
+ keys.each { |key| delete_object(key) }
end
private
+ def delete_object(key)
+ connection.delete_object(bucket_name, key)
+
+ # So far, only GoogleCloudStorage raises an exception when the file is not found.
+ # Other providers support idempotent requests and does not raise an error
+ # when the file is missing.
+ rescue ::Google::Apis::ClientError => e
+ Gitlab::ErrorTracking.log_exception(e)
+ end
+
def object_store
Gitlab.config.uploads.object_store
end
diff --git a/app/models/uploads/local.rb b/app/models/uploads/local.rb
index bd295a66838..9df69998991 100644
--- a/app/models/uploads/local.rb
+++ b/app/models/uploads/local.rb
@@ -55,3 +55,5 @@ module Uploads
end
end
end
+
+Uploads::Local.prepend_mod
diff --git a/app/models/user.rb b/app/models/user.rb
index 0e19e6e4a79..3ab5b7ee364 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -123,7 +123,7 @@ class User < ApplicationRecord
# Profile
has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :expired_and_unnotified_keys, -> { expired_and_not_notified }, class_name: 'Key'
+ has_many :expired_today_and_unnotified_keys, -> { expired_today_and_not_notified }, class_name: 'Key'
has_many :expiring_soon_and_unnotified_keys, -> { expiring_soon_and_not_notified }, class_name: 'Key'
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :group_deploy_keys
@@ -274,14 +274,21 @@ class User < ApplicationRecord
after_update :username_changed_hook, if: :saved_change_to_username?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
+ after_create :add_primary_email_to_emails!, if: :confirmed?
after_commit(on: :update) do
if previous_changes.key?('email')
- # Grab previous_email here since previous_changes changes after
- # #update_emails_with_primary_email and #update_notification_email are called
+ # Add the old primary email to Emails if not added already - this should be removed
+ # after the background migration for MR https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70872/ has completed,
+ # as the primary email is now added to Emails upon confirmation
+ # Issue to remove that: https://gitlab.com/gitlab-org/gitlab/-/issues/344134
previous_confirmed_at = previous_changes.key?('confirmed_at') ? previous_changes['confirmed_at'][0] : confirmed_at
previous_email = previous_changes[:email][0]
+ if previous_confirmed_at && !emails.exists?(email: previous_email)
+ # rubocop: disable CodeReuse/ServiceClass
+ Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: previous_confirmed_at)
+ # rubocop: enable CodeReuse/ServiceClass
+ end
- update_emails_with_primary_email(previous_confirmed_at, previous_email)
update_invalid_gpg_signatures
end
end
@@ -454,8 +461,8 @@ class User < ApplicationRecord
scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
scope :order_oldest_last_activity, -> { reorder(Gitlab::Database.nulls_first_order('last_activity_on', 'ASC')) }
scope :by_id_and_login, ->(id, login) { where(id: id).where('username = LOWER(:login) OR email = LOWER(:login)', login: login) }
- scope :dormant, -> { active.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) }
- scope :with_no_activity, -> { active.where(last_activity_on: nil) }
+ scope :dormant, -> { with_state(:active).human_or_service_user.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) }
+ scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil) }
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
scope :get_ids_by_username, -> (username) { where(username: username).pluck(:id) }
@@ -466,7 +473,11 @@ class User < ApplicationRecord
end
def active_for_authentication?
- super && can?(:log_in)
+ return false unless super
+
+ check_ldap_if_ldap_blocked!
+
+ can?(:log_in)
end
# The messages for these keys are defined in `devise.en.yml`
@@ -935,6 +946,8 @@ class User < ApplicationRecord
end
def unique_email
+ return if errors.added?(:email, _('has already been taken'))
+
if !emails.exists?(email: email) && Email.exists?(email: email)
errors.add(:email, _('has already been taken'))
end
@@ -963,24 +976,6 @@ class User < ApplicationRecord
skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
end
- # Note: the use of the Emails services will cause `saves` on the user object, running
- # through the callbacks again and can have side effects, such as the `previous_changes`
- # hash and `_was` variables getting munged.
- # By using an `after_commit` instead of `after_update`, we avoid the recursive callback
- # scenario, though it then requires us to use the `previous_changes` hash
- # rubocop: disable CodeReuse/ServiceClass
- def update_emails_with_primary_email(previous_confirmed_at, previous_email)
- primary_email_record = emails.find_by(email: email)
- Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
-
- # the original primary email was confirmed, and we want that to carry over. We don't
- # have access to the original confirmation values at this point, so just set confirmed_at
- Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: previous_confirmed_at)
-
- update_columns(confirmed_at: primary_email_record.confirmed_at) if primary_email_record&.confirmed_at
- end
- # rubocop: enable CodeReuse/ServiceClass
-
def update_invalid_gpg_signatures
gpg_keys.each(&:update_invalid_gpg_signatures)
end
@@ -1025,8 +1020,10 @@ class User < ApplicationRecord
end
# rubocop: enable CodeReuse/ServiceClass
- def remove_project_authorizations(project_ids)
- project_authorizations.where(project_id: project_ids).delete_all
+ def remove_project_authorizations(project_ids, per_batch = 1000)
+ project_ids.each_slice(per_batch) do |project_ids_batch|
+ project_authorizations.where(project_id: project_ids_batch).delete_all
+ end
end
def authorized_projects(min_access_level = nil)
@@ -1389,7 +1386,7 @@ class User < ApplicationRecord
all_emails << email unless temp_oauth_email?
all_emails << private_commit_email if include_private_email
all_emails.concat(emails.map(&:email))
- all_emails
+ all_emails.uniq
end
def verified_emails(include_private_email: true)
@@ -1397,7 +1394,7 @@ class User < ApplicationRecord
verified_emails << email if primary_email_verified?
verified_emails << private_commit_email if include_private_email
verified_emails.concat(emails.confirmed.pluck(:email))
- verified_emails
+ verified_emails.uniq
end
def public_verified_emails
@@ -1610,8 +1607,6 @@ class User < ApplicationRecord
true
end
- # TODO Please check all callers and remove allow_cross_joins_across_databases,
- # when https://gitlab.com/gitlab-org/gitlab/-/issues/336436 is done.
def ci_owned_runners
@ci_owned_runners ||= begin
project_runners = Ci::RunnerProject
@@ -1624,7 +1619,7 @@ class User < ApplicationRecord
.joins(:runner)
.select('ci_runners.*')
- Ci::Runner.from_union([project_runners, group_runners])
+ Ci::Runner.from_union([project_runners, group_runners]).allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336436')
end
end
@@ -1980,6 +1975,37 @@ class User < ApplicationRecord
ci_job_token_scope.present?
end
+ # override from Devise::Confirmable
+ #
+ # Add the primary email to user.emails (or confirm it if it was already
+ # present) when the primary email is confirmed.
+ def confirm(*args)
+ saved = super(*args)
+ return false unless saved
+
+ email_to_confirm = self.emails.find_by(email: self.email)
+
+ if email_to_confirm.present?
+ email_to_confirm.confirm(*args)
+ else
+ add_primary_email_to_emails!
+ end
+
+ saved
+ end
+
+ def user_project
+ strong_memoize(:user_project) do
+ personal_projects.find_by(path: username, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+
+ def user_readme
+ strong_memoize(:user_readme) do
+ user_project&.repository&.readme
+ end
+ end
+
protected
# override, from Devise::Validatable
@@ -2020,6 +2046,12 @@ class User < ApplicationRecord
'en'
end
+ # rubocop: disable CodeReuse/ServiceClass
+ def add_primary_email_to_emails!
+ Emails::CreateService.new(self, user: self, email: self.email).execute(confirmed_at: self.confirmed_at)
+ end
+ # rubocop: enable CodeReuse/ServiceClass
+
def notification_email_verified
return if notification_email.blank? || temp_oauth_email?
@@ -2153,6 +2185,13 @@ class User < ApplicationRecord
def ci_job_token_scope_cache_key
"users:#{id}:ci:job_token_scope"
end
+
+ # An `ldap_blocked` user will be unblocked if LDAP indicates they are allowed.
+ def check_ldap_if_ldap_blocked!
+ return unless ::Gitlab::Auth::Ldap::Config.enabled? && ldap_blocked?
+
+ ::Gitlab::Auth::Ldap::Access.allowed?(self)
+ end
end
User.prepend_mod_with('User')
diff --git a/app/models/user_status.rb b/app/models/user_status.rb
index 1c8634e47c3..7a803e8f1f6 100644
--- a/app/models/user_status.rb
+++ b/app/models/user_status.rb
@@ -22,7 +22,7 @@ class UserStatus < ApplicationRecord
enum availability: { not_set: 0, busy: 1 }
validates :user, presence: true
- validates :emoji, inclusion: { in: Gitlab::Emoji.emojis_names }
+ validates :emoji, 'gitlab/emoji_name': true
validates :message, length: { maximum: 100 }, allow_blank: true
scope :scheduled_for_cleanup, -> { where(arel_table[:clear_status_at].lteq(Time.current)) }
@@ -33,3 +33,5 @@ class UserStatus < ApplicationRecord
self.clear_status_at = CLEAR_STATUS_QUICK_OPTIONS[value]&.from_now
end
end
+
+UserStatus.prepend_mod_with('UserStatus')
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index a4cc43d1f13..556ee03605d 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -9,6 +9,7 @@ module Users
belongs_to :user
validates :holder_name, length: { maximum: 26 }
+ validates :network, length: { maximum: 32 }
validates :last_digits, allow_nil: true, numericality: {
greater_than_or_equal_to: 0, less_than_or_equal_to: 9999
}
@@ -17,7 +18,7 @@ module Users
self.class.where(
expiration_date: expiration_date,
last_digits: last_digits,
- holder_name: holder_name
+ network: network
).order(credit_card_validated_at: :desc).includes(:user)
end
end
diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb
index 8fe52ac7ecc..1f1eaacfe5c 100644
--- a/app/models/users/in_product_marketing_email.rb
+++ b/app/models/users/in_product_marketing_email.rb
@@ -22,7 +22,8 @@ module Users
experience: 4,
team_short: 5,
trial_short: 6,
- admin_verify: 7
+ admin_verify: 7,
+ invite_team: 8
}, _suffix: true
scope :without_track_and_series, -> (track, series) do
diff --git a/app/models/users_statistics.rb b/app/models/users_statistics.rb
index a903541f69a..a314ae8920b 100644
--- a/app/models/users_statistics.rb
+++ b/app/models/users_statistics.rb
@@ -3,12 +3,6 @@
class UsersStatistics < ApplicationRecord
scope :order_created_at_desc, -> { order(created_at: :desc) }
- class << self
- def latest
- order_created_at_desc.first
- end
- end
-
def active
[
without_groups_and_projects,
@@ -26,30 +20,26 @@ class UsersStatistics < ApplicationRecord
end
class << self
- def create_current_stats!
- stats_by_role = highest_role_stats
+ def latest
+ order_created_at_desc.first
+ end
- create!(
- without_groups_and_projects: without_groups_and_projects_stats,
- with_highest_role_guest: stats_by_role[:guest],
- with_highest_role_reporter: stats_by_role[:reporter],
- with_highest_role_developer: stats_by_role[:developer],
- with_highest_role_maintainer: stats_by_role[:maintainer],
- with_highest_role_owner: stats_by_role[:owner],
- bots: bot_stats,
- blocked: blocked_stats
- )
+ def create_current_stats!
+ create!(highest_role_stats)
end
private
def highest_role_stats
{
- owner: batch_count_for_access_level(Gitlab::Access::OWNER),
- maintainer: batch_count_for_access_level(Gitlab::Access::MAINTAINER),
- developer: batch_count_for_access_level(Gitlab::Access::DEVELOPER),
- reporter: batch_count_for_access_level(Gitlab::Access::REPORTER),
- guest: batch_count_for_access_level(Gitlab::Access::GUEST)
+ without_groups_and_projects: without_groups_and_projects_stats,
+ with_highest_role_guest: batch_count_for_access_level(Gitlab::Access::GUEST),
+ with_highest_role_reporter: batch_count_for_access_level(Gitlab::Access::REPORTER),
+ with_highest_role_developer: batch_count_for_access_level(Gitlab::Access::DEVELOPER),
+ with_highest_role_maintainer: batch_count_for_access_level(Gitlab::Access::MAINTAINER),
+ with_highest_role_owner: batch_count_for_access_level(Gitlab::Access::OWNER),
+ bots: bot_stats,
+ blocked: blocked_stats
}
end
diff --git a/app/models/webauthn_registration.rb b/app/models/webauthn_registration.rb
index 76f8faa11c7..71b50192e29 100644
--- a/app/models/webauthn_registration.rb
+++ b/app/models/webauthn_registration.rb
@@ -5,7 +5,8 @@
class WebauthnRegistration < ApplicationRecord
belongs_to :user
- validates :credential_xid, :public_key, :name, :counter, presence: true
+ validates :credential_xid, :public_key, :counter, presence: true
+ validates :name, length: { minimum: 0, allow_nil: false }
validates :counter,
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 64395f69c42..833d5b9bd34 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -75,6 +75,8 @@ class GroupPolicy < BasePolicy
with_scope :subject
condition(:has_project_with_service_desk_enabled) { @subject.has_project_with_service_desk_enabled? }
+ condition(:crm_enabled, score: 0, scope: :subject) { Feature.enabled?(:customer_relations, @subject) }
+
rule { can?(:read_group) & design_management_enabled }.policy do
enable :read_design_activity
end
@@ -113,8 +115,8 @@ class GroupPolicy < BasePolicy
enable :read_group_member
enable :read_custom_emoji
enable :read_counts
- enable :read_organization
- enable :read_contact
+ enable :read_crm_organization
+ enable :read_crm_contact
end
rule { ~public_group & ~has_access }.prevent :read_counts
@@ -134,8 +136,8 @@ class GroupPolicy < BasePolicy
enable :create_package
enable :create_package_settings
enable :developer_access
- enable :admin_organization
- enable :admin_contact
+ enable :admin_crm_organization
+ enable :admin_crm_contact
end
rule { reporter }.policy do
@@ -252,6 +254,13 @@ class GroupPolicy < BasePolicy
enable :read_label
end
+ rule { ~crm_enabled }.policy do
+ prevent :read_crm_contact
+ prevent :read_crm_organization
+ prevent :admin_crm_contact
+ prevent :admin_crm_organization
+ end
+
def access_level(for_any_session: false)
return GroupMember::NO_ACCESS if @user.nil?
return GroupMember::NO_ACCESS unless user_is_user?
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 575e532c615..c9c13b29643 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -12,6 +12,9 @@ class IssuePolicy < IssuablePolicy
@user && IssueCollection.new([@subject]).visible_to(@user).any?
end
+ desc "User can read contacts belonging to the issue group"
+ condition(:can_read_crm_contacts, scope: :subject) { @user.can?(:read_crm_contact, @subject.project.group) }
+
desc "Issue is confidential"
condition(:confidential, scope: :subject) { @subject.confidential? }
@@ -77,6 +80,10 @@ class IssuePolicy < IssuablePolicy
rule { ~persisted & can?(:create_issue) }.policy do
enable :set_confidentiality
end
+
+ rule { can?(:set_issue_metadata) & can_read_crm_contacts }.policy do
+ enable :set_issue_crm_contacts
+ end
end
IssuePolicy.prepend_mod_with('IssuePolicy')
diff --git a/app/policies/packages/helm/file_metadatum_policy.rb b/app/policies/packages/helm/file_metadatum_policy.rb
new file mode 100644
index 00000000000..4e0cb9046bf
--- /dev/null
+++ b/app/policies/packages/helm/file_metadatum_policy.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+module Packages
+ module Helm
+ class FileMetadatumPolicy < BasePolicy
+ delegate { @subject.package_file.package }
+ end
+ end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 87573c9ad13..d81db357162 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -47,6 +47,9 @@ class ProjectPolicy < BasePolicy
desc "Project is archived"
condition(:archived, scope: :subject, score: 0) { project.archived? }
+ desc "Project is in the process of being deleted"
+ condition(:pending_delete) { project.pending_delete? }
+
condition(:default_issues_tracker, scope: :subject) { project.default_issues_tracker? }
desc "Container registry is disabled"
@@ -248,7 +251,7 @@ class ProjectPolicy < BasePolicy
enable :read_insights
end
- rule { can?(:guest_access) & can?(:create_issue) }.enable :create_incident
+ rule { can?(:reporter_access) & can?(:create_issue) }.enable :create_incident
# These abilities are not allowed to admins that are not members of the project,
# that's why they are defined separately.
@@ -439,7 +442,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_freeze_period
enable :admin_feature_flags_client
enable :update_runners_registration_token
- enable :manage_project_google_cloud
+ enable :admin_project_google_cloud
end
rule { public_project & metrics_dashboard_allowed }.policy do
@@ -457,7 +460,13 @@ class ProjectPolicy < BasePolicy
prevent(*readonly_abilities)
readonly_features.each do |feature|
- prevent(*create_update_admin_destroy(feature))
+ prevent(*create_update_admin(feature))
+ end
+ end
+
+ rule { archived & ~pending_delete }.policy do
+ readonly_features.each do |feature|
+ prevent(:"destroy_#{feature}")
end
end
diff --git a/app/presenters/award_emoji_presenter.rb b/app/presenters/award_emoji_presenter.rb
index 8a7b58e0aba..9a1c8d6934d 100644
--- a/app/presenters/award_emoji_presenter.rb
+++ b/app/presenters/award_emoji_presenter.rb
@@ -4,24 +4,24 @@ class AwardEmojiPresenter < Gitlab::View::Presenter::Delegated
presents ::AwardEmoji, as: :award_emoji
def description
- as_emoji['description']
+ as_emoji&.description
end
def unicode
- as_emoji['unicode']
+ as_emoji&.hex
end
def emoji
- as_emoji['moji']
+ as_emoji&.codepoints
end
def unicode_version
- Gitlab::Emoji.emoji_unicode_version(award_emoji.name)
+ as_emoji&.unicode_version
end
private
def as_emoji
- @emoji ||= Gitlab::Emoji.emojis[award_emoji.name] || {}
+ @emoji ||= TanukiEmoji.find_by_alpha_code(award_emoji.name)
end
end
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index c198859aa4c..5835a77d0b9 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+require 'ipynbdiff'
class BlobPresenter < Gitlab::View::Presenter::Delegated
include ApplicationHelper
@@ -20,6 +21,17 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
)
end
+ def highlight_transformed(plain: nil)
+ load_all_blob_data
+
+ Gitlab::Highlight.highlight(
+ blob.path,
+ transformed_blob_data,
+ language: transformed_blob_language,
+ plain: plain
+ )
+ end
+
def plain_data
return if blob.binary?
@@ -50,6 +62,10 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
url_helpers.project_create_blob_path(project, ref_qualified_path)
end
+ def pipeline_editor_path
+ project_ci_pipeline_editor_path(project, branch_name: blob.commit_id) if can_collaborate_with_project?(project) && blob.path == project.ci_config_path_or_default
+ end
+
def fork_and_edit_path
fork_path_for_current_user(project, edit_blob_path)
end
@@ -107,4 +123,21 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
def language
blob.language_from_gitattributes
end
+
+ def transformed_blob_language
+ @transformed_blob_language ||= blob.path.ends_with?('.ipynb') ? 'md' : language
+ end
+
+ def transformed_blob_data
+ @transformed_blob ||= if blob.path.ends_with?('.ipynb') && blob.transformed_for_diff
+ IpynbDiff.transform(blob.data,
+ raise_errors: true,
+ options: { include_metadata: false, cell_decorator: :percent })
+ end
+
+ @transformed_blob ||= blob.data
+ rescue IpynbDiff::InvalidNotebookError => e
+ Gitlab::ErrorTracking.log_exception(e)
+ blob.data
+ end
end
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index b0066e2d7f0..8e1b675d051 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -33,11 +33,7 @@ module Ci
end
def runner_variables
- if Feature.enabled?(:variable_inside_variable, project, default_enabled: :yaml)
- variables.sort_and_expand_all(project, keep_undefined: true).to_runner_variables
- else
- variables.to_runner_variables
- end
+ variables.sort_and_expand_all(keep_undefined: true).to_runner_variables
end
def refspecs
diff --git a/app/presenters/packages/npm/package_presenter.rb b/app/presenters/packages/npm/package_presenter.rb
index b9595eb6647..9e3308c2573 100644
--- a/app/presenters/packages/npm/package_presenter.rb
+++ b/app/presenters/packages/npm/package_presenter.rb
@@ -5,26 +5,37 @@ module Packages
class PackagePresenter
include API::Helpers::RelatedResourcesHelpers
+ # Allowed fields are those defined in the abbreviated form
+ # defined here: https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
+ # except: name, version, dist, dependencies and xDependencies. Those are generated by this presenter.
+ PACKAGE_JSON_ALLOWED_FIELDS = %w[deprecated bin directories dist engines _hasShrinkwrap].freeze
+
attr_reader :name, :packages
- def initialize(name, packages)
+ def initialize(name, packages, include_metadata: false)
@name = name
@packages = packages
+ @include_metadata = include_metadata
end
def versions
package_versions = {}
packages.each_batch do |relation|
- relation.including_dependency_links
- .preload_files
- .each do |package|
- package_file = package.package_files.last
+ batched_packages = relation.including_dependency_links
+ .preload_files
+
+ if @include_metadata
+ batched_packages = batched_packages.preload_npm_metadatum
+ end
+
+ batched_packages.each do |package|
+ package_file = package.package_files.last
- next unless package_file
+ next unless package_file
- package_versions[package.version] = build_package_version(package, package_file)
- end
+ package_versions[package.version] = build_package_version(package, package_file)
+ end
end
package_versions
@@ -41,14 +52,14 @@ module Packages
end
def build_package_version(package, package_file)
- {
+ abbreviated_package_json(package).merge(
name: package.name,
version: package.version,
dist: {
shasum: package_file.file_sha1,
tarball: tarball_url(package, package_file)
}
- }.tap do |package_version|
+ ).tap do |package_version|
package_version.merge!(build_package_dependencies(package))
end
end
@@ -79,6 +90,13 @@ module Packages
Packages::Tag.for_packages(packages)
.preload_package
end
+
+ def abbreviated_package_json(package)
+ return {} unless @include_metadata
+
+ json = package.npm_metadatum&.package_json || {}
+ json.slice(*PACKAGE_JSON_ALLOWED_FIELDS)
+ end
end
end
end
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index bbd8c715f5c..ec66f9bdd4f 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -249,33 +249,23 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
strong_memoize(:upload_anchor_data) do
next unless can_current_user_push_to_default_branch?
- experiment(:empty_repo_upload, project: project) do |e|
- e.use {}
- e.try do
- AnchorData.new(false,
- statistic_icon('upload') + _('Upload file'),
- '#modal-upload-blob',
- 'js-upload-file-experiment-trigger',
- nil,
- nil,
- {
- 'target_branch' => default_branch_or_main,
- 'original_branch' => default_branch_or_main,
- 'can_push_code' => 'true',
- 'path' => project_create_blob_path(project, default_branch_or_main),
- 'project_path' => project.full_path
- }
- )
- end
- e.run
- end
+ AnchorData.new(false,
+ statistic_icon('upload') + _('Upload file'),
+ '#modal-upload-blob',
+ 'js-upload-file-trigger',
+ nil,
+ nil,
+ {
+ 'target_branch' => default_branch_or_main,
+ 'original_branch' => default_branch_or_main,
+ 'can_push_code' => 'true',
+ 'path' => project_create_blob_path(project, default_branch_or_main),
+ 'project_path' => project.full_path
+ }
+ )
end
end
- def empty_repo_upload_experiment?
- upload_anchor_data.present?
- end
-
def new_file_anchor_data
if can_current_user_push_to_default_branch?
new_file_path = empty_repo? ? ide_edit_path(project, default_branch_or_main) : project_new_blob_path(project, default_branch_or_main)
diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb
index c919c7f4c60..dac42af38bf 100644
--- a/app/presenters/release_presenter.rb
+++ b/app/presenters/release_presenter.rb
@@ -22,8 +22,6 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
end
def self_url
- return unless can_download_code?
-
project_release_url(project, release)
end
@@ -64,7 +62,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
delegator_override :name
def name
- can_download_code? ? release.name : "Release-#{release.id}"
+ release.name
end
def download_url(filepath)
diff --git a/app/serializers/alert_management/alert_entity.rb b/app/serializers/alert_management/alert_entity.rb
new file mode 100644
index 00000000000..6871da44887
--- /dev/null
+++ b/app/serializers/alert_management/alert_entity.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ class AlertEntity < Grape::Entity
+ expose :iid
+ expose :title
+ end
+end
diff --git a/app/serializers/alert_management/alert_serializer.rb b/app/serializers/alert_management/alert_serializer.rb
new file mode 100644
index 00000000000..89815bf6510
--- /dev/null
+++ b/app/serializers/alert_management/alert_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ class AlertSerializer < BaseSerializer
+ entity AlertManagement::AlertEntity
+ end
+end
diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb
index 62828fc1428..dc42d7f52ad 100644
--- a/app/serializers/analytics_summary_entity.rb
+++ b/app/serializers/analytics_summary_entity.rb
@@ -4,6 +4,7 @@ class AnalyticsSummaryEntity < Grape::Entity
expose :value, safe: true
expose :title
expose :unit, if: { with_unit: true }
+ expose :links
private
diff --git a/app/serializers/merge_request_sidebar_extras_entity.rb b/app/serializers/merge_request_sidebar_extras_entity.rb
index 1a0111fe5d0..c211f9c1608 100644
--- a/app/serializers/merge_request_sidebar_extras_entity.rb
+++ b/app/serializers/merge_request_sidebar_extras_entity.rb
@@ -2,10 +2,10 @@
class MergeRequestSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees do |merge_request, options|
- MergeRequestUserEntity.represent(merge_request.assignees, options.merge(merge_request: merge_request))
+ MergeRequestUserEntity.represent(merge_request.assignees, options.merge(merge_request: merge_request, type: :assignees))
end
expose :reviewers do |merge_request, options|
- MergeRequestUserEntity.represent(merge_request.reviewers, options.merge(merge_request: merge_request))
+ MergeRequestUserEntity.represent(merge_request.reviewers, options.merge(merge_request: merge_request, type: :reviewers))
end
end
diff --git a/app/serializers/merge_request_user_entity.rb b/app/serializers/merge_request_user_entity.rb
index 66672494bd9..97912656bbb 100644
--- a/app/serializers/merge_request_user_entity.rb
+++ b/app/serializers/merge_request_user_entity.rb
@@ -16,10 +16,12 @@ class MergeRequestUserEntity < ::API::Entities::UserBasic
request.current_user&.can?(:update_merge_request, options[:merge_request])
end
- expose :reviewed, if: satisfies(:present?, :allows_reviewers?) do |reviewer, options|
- reviewer = options[:merge_request].find_reviewer(reviewer)
+ expose :reviewed, if: satisfies(:present?, :allows_reviewers?) do |user, options|
+ find_reviewer_or_assignee(user, options)&.reviewed?
+ end
- reviewer&.reviewed?
+ expose :attention_requested, if: satisfies(:present?, :allows_reviewers?, :attention_requested_enabled?) do |user, options|
+ find_reviewer_or_assignee(user, options)&.attention_requested?
end
expose :approved, if: satisfies(:present?) do |user, options|
@@ -27,6 +29,16 @@ class MergeRequestUserEntity < ::API::Entities::UserBasic
# makes one query per merge request, whereas #approved_by? makes one per user
options[:merge_request].approvals.any? { |app| app.user_id == user.id }
end
+
+ private
+
+ def find_reviewer_or_assignee(user, options)
+ if options[:type] == :reviewers
+ options[:merge_request].find_reviewer(user)
+ else
+ options[:merge_request].find_assignee(user)
+ end
+ end
end
MergeRequestUserEntity.prepend_mod_with('MergeRequestUserEntity')
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 1e4289ce774..bd60d60c8db 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -48,7 +48,7 @@ class MergeRequestWidgetEntity < Grape::Entity
end
expose :conflicts_docs_path do |merge_request|
- help_page_path('user/project/merge_requests/resolve_conflicts.md')
+ help_page_path('user/project/merge_requests/conflicts.md')
end
expose :reviewing_and_managing_merge_requests_docs_path do |merge_request|
@@ -64,14 +64,12 @@ class MergeRequestWidgetEntity < Grape::Entity
end
expose :merge_request_add_ci_config_path, if: ->(mr, _) { can_add_ci_config_path?(mr) } do |merge_request|
- project_new_blob_path(
- merge_request.source_project,
- merge_request.source_branch,
- file_name: '.gitlab-ci.yml',
- commit_message: s_("CommitMessage|Add %{file_name}") % { file_name: Gitlab::FileDetector::PATTERNS[:gitlab_ci] },
- mr_path: merge_request_path(merge_request),
- suggest_gitlab_ci_yml: true
- )
+ project = merge_request.source_project
+ params = {
+ branch_name: merge_request.source_branch,
+ add_new_config_file: true
+ }
+ project_ci_pipeline_editor_path(project, params)
end
expose :user_callouts_path do |_merge_request|
@@ -177,7 +175,6 @@ class MergeRequestWidgetEntity < Grape::Entity
def can_add_ci_config_path?(merge_request)
merge_request.open? &&
merge_request.source_branch_exists? &&
- merge_request.source_project&.uses_default_ci_config? &&
!merge_request.source_project.has_ci? &&
merge_request.commits_count > 0 &&
can?(current_user, :read_build, merge_request.source_project) &&
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index 8308e954c06..8f189f14dea 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -51,6 +51,10 @@ class NoteEntity < API::Entities::Note
SystemNoteHelper.system_note_icon_name(note)
end
+ expose :outdated_line_change_path, if: -> (note, _) { note.system? && note.change_position&.line_range && Feature.enabled?(:display_outdated_line_diff, note.project, default_enabled: :yaml) } do |note|
+ outdated_line_change_namespace_project_note_path(namespace_id: note.project.namespace, project_id: note.project, id: note)
+ end
+
expose :is_noteable_author do |note|
note.noteable_author?(request.noteable)
end
diff --git a/app/serializers/service_field_entity.rb b/app/serializers/service_field_entity.rb
index 960e216906e..aad9db5ffea 100644
--- a/app/serializers/service_field_entity.rb
+++ b/app/serializers/service_field_entity.rb
@@ -4,7 +4,7 @@ class ServiceFieldEntity < Grape::Entity
include RequestAwareEntity
include Gitlab::Utils::StrongMemoize
- expose :type, :name, :placeholder, :required, :choices
+ expose :type, :name, :placeholder, :required, :choices, :checkbox_label
expose :title do |field|
non_empty_password?(field) ? field[:non_empty_password_title] : field[:title]
diff --git a/app/services/alert_management/process_prometheus_alert_service.rb b/app/services/alert_management/process_prometheus_alert_service.rb
index 605ab7a1869..1b377a3d367 100644
--- a/app/services/alert_management/process_prometheus_alert_service.rb
+++ b/app/services/alert_management/process_prometheus_alert_service.rb
@@ -4,6 +4,7 @@ module AlertManagement
class ProcessPrometheusAlertService
extend ::Gitlab::Utils::Override
include ::AlertManagement::AlertProcessing
+ include ::AlertManagement::Responses
def initialize(project, payload)
@project = project
@@ -18,7 +19,7 @@ module AlertManagement
complete_post_processing_tasks
- ServiceResponse.success
+ success(alert)
end
private
@@ -40,9 +41,5 @@ module AlertManagement
def resolving_alert?
incoming_payload.resolved?
end
-
- def bad_request
- ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
- end
end
end
diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb
index 558798c830d..563d4a924fc 100644
--- a/app/services/audit_event_service.rb
+++ b/app/services/audit_event_service.rb
@@ -119,6 +119,10 @@ class AuditEventService
event
end
+ def stream_event_to_external_destinations(_event)
+ # Defined in EE
+ end
+
def log_authentication_event_to_database
return unless Gitlab::Database.read_write? && authentication_event?
@@ -130,6 +134,7 @@ class AuditEventService
def save_or_track(event)
event.save!
+ stream_event_to_external_destinations(event)
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, audit_event_type: event.class.to_s)
end
diff --git a/app/services/authorized_project_update/project_access_changed_service.rb b/app/services/authorized_project_update/project_access_changed_service.rb
new file mode 100644
index 00000000000..62bf4ced1ae
--- /dev/null
+++ b/app/services/authorized_project_update/project_access_changed_service.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module AuthorizedProjectUpdate
+ class ProjectAccessChangedService
+ def initialize(project_ids)
+ @project_ids = Array.wrap(project_ids)
+ end
+
+ def execute(blocking: true)
+ bulk_args = @project_ids.map { |id| [id] }
+
+ if blocking
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.bulk_perform_and_wait(bulk_args)
+ else
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
+ end
+ end
+ end
+end
diff --git a/app/services/award_emojis/base_service.rb b/app/services/award_emojis/base_service.rb
index a677d03a221..626e26d63b5 100644
--- a/app/services/award_emojis/base_service.rb
+++ b/app/services/award_emojis/base_service.rb
@@ -14,7 +14,7 @@ module AwardEmojis
private
def normalize_name(name)
- Gitlab::Emoji.normalize_emoji_name(name)
+ TanukiEmoji.find_by_alpha_code(name)&.name || name
end
# Provide more error state data than what BaseService allows.
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 275ebcc7bcd..c7380768e32 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -6,13 +6,7 @@
# and existing service will use these one by one.
# After all are migrated, we can remove this class.
#
-# New services should consider inheriting from:
-#
-# - BaseContainerService for services scoped by container (project or group)
-# - BaseProjectService for services scoped to projects
-# - BaseGroupService for services scoped to groups
-#
-# or, create a new base class and update this comment.
+# For new services, please see https://docs.gitlab.com/ee/development/reusing_abstractions.html#service-classes
class BaseService
include BaseServiceUtility
include Gitlab::Experiment::Dsl
diff --git a/app/services/bulk_imports/file_download_service.rb b/app/services/bulk_imports/file_download_service.rb
index 9a301c260a9..d08dc72e30b 100644
--- a/app/services/bulk_imports/file_download_service.rb
+++ b/app/services/bulk_imports/file_download_service.rb
@@ -7,8 +7,16 @@ module BulkImports
REMOTE_FILENAME_PATTERN = %r{filename="(?<filename>[^"]+)"}.freeze
FILENAME_SIZE_LIMIT = 255 # chars before the extension
-
- def initialize(configuration:, relative_url:, dir:, file_size_limit:, allowed_content_types:, filename: nil)
+ DEFAULT_FILE_SIZE_LIMIT = 5.gigabytes
+ DEFAULT_ALLOWED_CONTENT_TYPES = %w(application/gzip application/octet-stream).freeze
+
+ def initialize(
+ configuration:,
+ relative_url:,
+ dir:,
+ file_size_limit: DEFAULT_FILE_SIZE_LIMIT,
+ allowed_content_types: DEFAULT_ALLOWED_CONTENT_TYPES,
+ filename: nil)
@configuration = configuration
@relative_url = relative_url
@filename = filename
diff --git a/app/services/bulk_update_integration_service.rb b/app/services/bulk_update_integration_service.rb
index 45465ba3946..29c4d0cc220 100644
--- a/app/services/bulk_update_integration_service.rb
+++ b/app/services/bulk_update_integration_service.rb
@@ -12,7 +12,7 @@ class BulkUpdateIntegrationService
Integration.where(id: batch_ids).update_all(integration_hash)
if integration.data_fields_present?
- integration.data_fields.class.where(service_id: batch_ids).update_all(data_fields_hash)
+ integration.data_fields.class.where(data_fields_foreign_key => batch_ids).update_all(data_fields_hash)
end
end
end
@@ -22,6 +22,11 @@ class BulkUpdateIntegrationService
attr_reader :integration, :batch
+ # service_id or integration_id
+ def data_fields_foreign_key
+ integration.data_fields.class.reflections['integration'].foreign_key
+ end
+
def integration_hash
integration.to_integration_hash.tap { |json| json['inherit_from_id'] = integration.inherit_from_id || integration.id }
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index ba9665555cc..540e8f7b970 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -25,6 +25,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::StopDryRun,
Gitlab::Ci::Pipeline::Chain::Create,
+ Gitlab::Ci::Pipeline::Chain::CreateCrossDatabaseAssociations,
Gitlab::Ci::Pipeline::Chain::Limit::Activity,
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity,
Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines,
diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb
index 476c7523d60..6fbde5d291c 100644
--- a/app/services/ci/destroy_pipeline_service.rb
+++ b/app/services/ci/destroy_pipeline_service.rb
@@ -12,7 +12,9 @@ module Ci
# Ci::Pipeline#destroy triggers `use_fast_destroy :job_artifacts` and
# ci_builds has ON DELETE CASCADE to ci_pipelines. The pipeline, the builds,
# job and pipeline artifacts all get destroyed here.
- pipeline.reset.destroy!
+ ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/345664') do
+ pipeline.reset.destroy!
+ end
ServiceResponse.success(message: 'Pipeline not found')
rescue ActiveRecord::RecordNotFound
diff --git a/app/services/ci/external_pull_requests/create_pipeline_service.rb b/app/services/ci/external_pull_requests/create_pipeline_service.rb
index dd93ca4708e..66127c94d35 100644
--- a/app/services/ci/external_pull_requests/create_pipeline_service.rb
+++ b/app/services/ci/external_pull_requests/create_pipeline_service.rb
@@ -16,14 +16,9 @@ module Ci
private
def create_pipeline_for(pull_request)
- if ::Feature.enabled?(:ci_create_external_pr_pipeline_async, project, default_enabled: :yaml)
- Ci::ExternalPullRequests::CreatePipelineWorker.perform_async(
- project.id, current_user.id, pull_request.id
- )
- else
- Ci::CreatePipelineService.new(project, current_user, create_params(pull_request))
- .execute(:external_pull_request_event, external_pull_request: pull_request)
- end
+ Ci::ExternalPullRequests::CreatePipelineWorker.perform_async(
+ project.id, current_user.id, pull_request.id
+ )
end
def create_params(pull_request)
diff --git a/app/services/ci/generate_kubeconfig_service.rb b/app/services/ci/generate_kubeconfig_service.rb
new file mode 100644
index 00000000000..18f68c0ff09
--- /dev/null
+++ b/app/services/ci/generate_kubeconfig_service.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Ci
+ class GenerateKubeconfigService
+ def initialize(build)
+ @build = build
+ @template = Gitlab::Kubernetes::Kubeconfig::Template.new
+ end
+
+ def execute
+ template.add_cluster(
+ name: cluster_name,
+ url: Gitlab::Kas.tunnel_url
+ )
+
+ agents.each do |agent|
+ user = user_name(agent)
+
+ template.add_user(
+ name: user,
+ token: agent_token(agent)
+ )
+
+ template.add_context(
+ name: context_name(agent),
+ cluster: cluster_name,
+ user: user
+ )
+ end
+
+ template
+ end
+
+ private
+
+ attr_reader :build, :template
+
+ def agents
+ build.pipeline.authorized_cluster_agents
+ end
+
+ def cluster_name
+ 'gitlab'
+ end
+
+ def user_name(agent)
+ ['agent', agent.id].join(delimiter)
+ end
+
+ def context_name(agent)
+ [agent.project.full_path, agent.name].join(delimiter)
+ end
+
+ def agent_token(agent)
+ ['ci', agent.id, build.token].join(delimiter)
+ end
+
+ def delimiter
+ ':'
+ end
+ end
+end
diff --git a/app/services/ci/job_artifacts/create_service.rb b/app/services/ci/job_artifacts/create_service.rb
index 9fc7c3b4d40..7c67a2e175d 100644
--- a/app/services/ci/job_artifacts/create_service.rb
+++ b/app/services/ci/job_artifacts/create_service.rb
@@ -19,6 +19,7 @@ module Ci
def initialize(job)
@job = job
@project = job.project
+ @pipeline = job.pipeline if ::Feature.enabled?(:ci_update_unlocked_job_artifacts, @project)
end
def authorize(artifact_type:, filesize: nil)
@@ -53,7 +54,7 @@ module Ci
private
- attr_reader :job, :project
+ attr_reader :job, :project, :pipeline
def validate_requirements(artifact_type:, filesize:)
return too_large_error if too_large?(artifact_type, filesize)
@@ -85,34 +86,38 @@ module Ci
expire_in = params['expire_in'] ||
Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in
- artifact = Ci::JobArtifact.new(
+ artifact_attributes = {
job_id: job.id,
project: project,
- file: artifacts_file,
- file_type: params[:artifact_type],
- file_format: params[:artifact_format],
- file_sha256: artifacts_file.sha256,
- expire_in: expire_in)
+ expire_in: expire_in
+ }
+
+ artifact_attributes[:locked] = pipeline.locked if ::Feature.enabled?(:ci_update_unlocked_job_artifacts, project)
+
+ artifact = Ci::JobArtifact.new(
+ artifact_attributes.merge(
+ file: artifacts_file,
+ file_type: params[:artifact_type],
+ file_format: params[:artifact_format],
+ file_sha256: artifacts_file.sha256
+ )
+ )
artifact_metadata = if metadata_file
Ci::JobArtifact.new(
- job_id: job.id,
- project: project,
- file: metadata_file,
- file_type: :metadata,
- file_format: :gzip,
- file_sha256: metadata_file.sha256,
- expire_in: expire_in)
+ artifact_attributes.merge(
+ file: metadata_file,
+ file_type: :metadata,
+ file_format: :gzip,
+ file_sha256: metadata_file.sha256
+ )
+ )
end
[artifact, artifact_metadata]
end
def parse_artifact(artifact)
- unless Feature.enabled?(:ci_synchronous_artifact_parsing, project, default_enabled: true)
- return success
- end
-
case artifact.file_type
when 'dotenv' then parse_dotenv_artifact(artifact)
else success
diff --git a/app/services/ci/job_artifacts/destroy_all_expired_service.rb b/app/services/ci/job_artifacts/destroy_all_expired_service.rb
index 3e9cc95d135..e4f65736a58 100644
--- a/app/services/ci/job_artifacts/destroy_all_expired_service.rb
+++ b/app/services/ci/job_artifacts/destroy_all_expired_service.rb
@@ -24,7 +24,11 @@ module Ci
# which is scheduled every 7 minutes.
def execute
in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
- destroy_job_artifacts_with_slow_iteration(Time.current)
+ if ::Feature.enabled?(:ci_destroy_unlocked_job_artifacts)
+ destroy_unlocked_job_artifacts(Time.current)
+ else
+ destroy_job_artifacts_with_slow_iteration(Time.current)
+ end
end
@removed_artifacts_count
@@ -32,13 +36,21 @@ module Ci
private
+ def destroy_unlocked_job_artifacts(start_at)
+ loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
+ artifacts = Ci::JobArtifact.expired_before(start_at).artifact_unlocked.limit(BATCH_SIZE)
+ service_response = destroy_batch(artifacts)
+ @removed_artifacts_count += service_response[:destroyed_artifacts_count]
+ end
+ end
+
def destroy_job_artifacts_with_slow_iteration(start_at)
Ci::JobArtifact.expired_before(start_at).each_batch(of: BATCH_SIZE, column: :expire_at, order: :desc) do |relation, index|
# For performance reasons, join with ci_pipelines after the batch is queried.
# See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47496
artifacts = relation.unlocked
- service_response = destroy_batch_async(artifacts)
+ service_response = destroy_batch(artifacts)
@removed_artifacts_count += service_response[:destroyed_artifacts_count]
break if loop_timeout?(start_at)
@@ -46,7 +58,7 @@ module Ci
end
end
- def destroy_batch_async(artifacts)
+ def destroy_batch(artifacts)
Ci::JobArtifacts::DestroyBatchService.new(artifacts).execute
end
diff --git a/app/services/ci/job_artifacts/destroy_batch_service.rb b/app/services/ci/job_artifacts/destroy_batch_service.rb
index 8536b88ccc0..866b40c32d8 100644
--- a/app/services/ci/job_artifacts/destroy_batch_service.rb
+++ b/app/services/ci/job_artifacts/destroy_batch_service.rb
@@ -26,15 +26,18 @@ module Ci
def execute(update_stats: true)
return success(destroyed_artifacts_count: 0, statistics_updates: {}) if @job_artifacts.empty?
+ destroy_related_records(@job_artifacts)
+
Ci::DeletedObject.transaction do
Ci::DeletedObject.bulk_import(@job_artifacts, @pick_up_at)
Ci::JobArtifact.id_in(@job_artifacts.map(&:id)).delete_all
- destroy_related_records(@job_artifacts)
end
+ after_batch_destroy_hook(@job_artifacts)
+
# This is executed outside of the transaction because it depends on Redis
update_project_statistics! if update_stats
- increment_monitoring_statistics(artifacts_count)
+ increment_monitoring_statistics(artifacts_count, artifacts_bytes)
success(destroyed_artifacts_count: artifacts_count,
statistics_updates: affected_project_statistics)
@@ -43,9 +46,12 @@ module Ci
private
- # This method is implemented in EE and it must do only database work
+ # Overriden in EE
def destroy_related_records(artifacts); end
+ # Overriden in EE
+ def after_batch_destroy_hook(artifacts); end
+
# using ! here since this can't be called inside a transaction
def update_project_statistics!
affected_project_statistics.each do |project, delta|
@@ -63,8 +69,9 @@ module Ci
end
end
- def increment_monitoring_statistics(size)
- metrics.increment_destroyed_artifacts(size)
+ def increment_monitoring_statistics(size, bytes)
+ metrics.increment_destroyed_artifacts_count(size)
+ metrics.increment_destroyed_artifacts_bytes(bytes)
end
def metrics
@@ -76,6 +83,12 @@ module Ci
@job_artifacts.count
end
end
+
+ def artifacts_bytes
+ strong_memoize(:artifacts_bytes) do
+ @job_artifacts.sum { |artifact| artifact.try(:size) || 0 }
+ end
+ end
end
end
end
diff --git a/app/services/ci/parse_dotenv_artifact_service.rb b/app/services/ci/parse_dotenv_artifact_service.rb
index 2ee9be476bb..725ecbcce5d 100644
--- a/app/services/ci/parse_dotenv_artifact_service.rb
+++ b/app/services/ci/parse_dotenv_artifact_service.rb
@@ -2,8 +2,7 @@
module Ci
class ParseDotenvArtifactService < ::BaseService
- MAX_ACCEPTABLE_DOTENV_SIZE = 5.kilobytes
- MAX_ACCEPTABLE_VARIABLES_COUNT = 20
+ include ::Gitlab::Utils::StrongMemoize
SizeLimitError = Class.new(StandardError)
ParserError = Class.new(StandardError)
@@ -27,9 +26,9 @@ module Ci
raise ArgumentError, 'Artifact is not dotenv file type'
end
- unless artifact.file.size < MAX_ACCEPTABLE_DOTENV_SIZE
+ unless artifact.file.size < dotenv_size_limit
raise SizeLimitError,
- "Dotenv Artifact Too Big. Maximum Allowable Size: #{MAX_ACCEPTABLE_DOTENV_SIZE}"
+ "Dotenv Artifact Too Big. Maximum Allowable Size: #{dotenv_size_limit}"
end
end
@@ -45,9 +44,9 @@ module Ci
end
end
- if variables.size > MAX_ACCEPTABLE_VARIABLES_COUNT
+ if variables.size > dotenv_variable_limit
raise SizeLimitError,
- "Dotenv files cannot have more than #{MAX_ACCEPTABLE_VARIABLES_COUNT} variables"
+ "Dotenv files cannot have more than #{dotenv_variable_limit} variables"
end
variables
@@ -60,5 +59,13 @@ module Ci
result.each(&:strip!)
end
+
+ def dotenv_variable_limit
+ strong_memoize(:dotenv_variable_limit) { project.actual_limits.dotenv_variables }
+ end
+
+ def dotenv_size_limit
+ strong_memoize(:dotenv_size_limit) { project.actual_limits.dotenv_size }
+ end
end
end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 07cfbb9ce3c..ebb07de9d29 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -63,7 +63,7 @@ module Ci
def clone_build(build)
project.builds.new(build_attributes(build)).tap do |new_build|
- new_build.assign_attributes(::Gitlab::Ci::Pipeline::Seed::Build.environment_attributes_for(new_build))
+ new_build.assign_attributes(deployment_attributes_for(new_build, build))
end
end
@@ -75,6 +75,11 @@ module Ci
attributes[:user] = current_user
attributes
end
+
+ def deployment_attributes_for(new_build, old_build)
+ ::Gitlab::Ci::Pipeline::Seed::Build
+ .deployment_attributes_for(new_build, old_build.persisted_environment)
+ end
end
end
diff --git a/app/services/ci/unlock_artifacts_service.rb b/app/services/ci/unlock_artifacts_service.rb
index 7c169cb8395..30da31ba8ec 100644
--- a/app/services/ci/unlock_artifacts_service.rb
+++ b/app/services/ci/unlock_artifacts_service.rb
@@ -5,22 +5,84 @@ module Ci
BATCH_SIZE = 100
def execute(ci_ref, before_pipeline = nil)
- query = <<~SQL.squish
- UPDATE "ci_pipelines"
- SET "locked" = #{::Ci::Pipeline.lockeds[:unlocked]}
- WHERE "ci_pipelines"."id" in (
- #{collect_pipelines(ci_ref, before_pipeline).select(:id).to_sql}
- LIMIT #{BATCH_SIZE}
- FOR UPDATE SKIP LOCKED
- )
- RETURNING "ci_pipelines"."id";
- SQL
-
- loop do
- break if Ci::Pipeline.connection.exec_query(query).empty?
+ results = {
+ unlocked_pipelines: 0,
+ unlocked_job_artifacts: 0
+ }
+
+ if ::Feature.enabled?(:ci_update_unlocked_job_artifacts, ci_ref.project)
+ loop do
+ unlocked_pipelines = []
+ unlocked_job_artifacts = []
+
+ ::Ci::Pipeline.transaction do
+ unlocked_pipelines = unlock_pipelines(ci_ref, before_pipeline)
+ unlocked_job_artifacts = unlock_job_artifacts(unlocked_pipelines)
+ end
+
+ break if unlocked_pipelines.empty?
+
+ results[:unlocked_pipelines] += unlocked_pipelines.length
+ results[:unlocked_job_artifacts] += unlocked_job_artifacts.length
+ end
+ else
+ query = <<~SQL.squish
+ UPDATE "ci_pipelines"
+ SET "locked" = #{::Ci::Pipeline.lockeds[:unlocked]}
+ WHERE "ci_pipelines"."id" in (
+ #{collect_pipelines(ci_ref, before_pipeline).select(:id).to_sql}
+ LIMIT #{BATCH_SIZE}
+ FOR UPDATE SKIP LOCKED
+ )
+ RETURNING "ci_pipelines"."id";
+ SQL
+
+ loop do
+ unlocked_pipelines = Ci::Pipeline.connection.exec_query(query)
+
+ break if unlocked_pipelines.empty?
+
+ results[:unlocked_pipelines] += unlocked_pipelines.length
+ end
end
+
+ results
end
+ # rubocop:disable CodeReuse/ActiveRecord
+ def unlock_job_artifacts_query(pipeline_ids)
+ ci_job_artifacts = ::Ci::JobArtifact.arel_table
+
+ build_ids = ::Ci::Build.select(:id).where(commit_id: pipeline_ids)
+
+ returning = Arel::Nodes::Grouping.new(ci_job_artifacts[:id])
+
+ Arel::UpdateManager.new
+ .table(ci_job_artifacts)
+ .where(ci_job_artifacts[:job_id].in(Arel.sql(build_ids.to_sql)))
+ .set([[ci_job_artifacts[:locked], ::Ci::JobArtifact.lockeds[:unlocked]]])
+ .to_sql + " RETURNING #{returning.to_sql}"
+ end
+ # rubocop:enable CodeReuse/ActiveRecord
+
+ # rubocop:disable CodeReuse/ActiveRecord
+ def unlock_pipelines_query(ci_ref, before_pipeline)
+ ci_pipelines = ::Ci::Pipeline.arel_table
+
+ pipelines_scope = ci_ref.pipelines.artifacts_locked
+ pipelines_scope = pipelines_scope.before_pipeline(before_pipeline) if before_pipeline
+ pipelines_scope = pipelines_scope.select(:id).limit(BATCH_SIZE).lock('FOR UPDATE SKIP LOCKED')
+
+ returning = Arel::Nodes::Grouping.new(ci_pipelines[:id])
+
+ Arel::UpdateManager.new
+ .table(ci_pipelines)
+ .where(ci_pipelines[:id].in(Arel.sql(pipelines_scope.to_sql)))
+ .set([[ci_pipelines[:locked], ::Ci::Pipeline.lockeds[:unlocked]]])
+ .to_sql + " RETURNING #{returning.to_sql}"
+ end
+ # rubocop:enable CodeReuse/ActiveRecord
+
private
def collect_pipelines(ci_ref, before_pipeline)
@@ -29,5 +91,17 @@ module Ci
pipeline_scope.artifacts_locked
end
+
+ def unlock_job_artifacts(pipelines)
+ return if pipelines.empty?
+
+ ::Ci::JobArtifact.connection.exec_query(
+ unlock_job_artifacts_query(pipelines.rows.flatten)
+ )
+ end
+
+ def unlock_pipelines(ci_ref, before_pipeline)
+ ::Ci::Pipeline.connection.exec_query(unlock_pipelines_query(ci_ref, before_pipeline))
+ end
end
end
diff --git a/app/services/ci/update_build_state_service.rb b/app/services/ci/update_build_state_service.rb
index 3b403f92486..826d9a2eda3 100644
--- a/app/services/ci/update_build_state_service.rb
+++ b/app/services/ci/update_build_state_service.rb
@@ -73,11 +73,11 @@ module Ci
::Gitlab::Ci::Trace::Checksum.new(build).then do |checksum|
unless checksum.valid?
metrics.increment_trace_operation(operation: :invalid)
- metrics.increment_error_counter(type: :chunks_invalid_checksum)
+ metrics.increment_error_counter(error_reason: :chunks_invalid_checksum)
if checksum.corrupted?
metrics.increment_trace_operation(operation: :corrupted)
- metrics.increment_error_counter(type: :chunks_invalid_size)
+ metrics.increment_error_counter(error_reason: :chunks_invalid_size)
end
next unless log_invalid_chunks?
diff --git a/app/services/clusters/agents/refresh_authorization_service.rb b/app/services/clusters/agents/refresh_authorization_service.rb
index 7f401eef720..54b90a7304c 100644
--- a/app/services/clusters/agents/refresh_authorization_service.rb
+++ b/app/services/clusters/agents/refresh_authorization_service.rb
@@ -86,7 +86,7 @@ module Clusters
if group_root_ancestor?
root_ancestor.all_projects
else
- ::Project.none
+ ::Project.id_in(project.id)
end
end
diff --git a/app/services/clusters/applications/prometheus_health_check_service.rb b/app/services/clusters/applications/prometheus_health_check_service.rb
deleted file mode 100644
index eda47f56e72..00000000000
--- a/app/services/clusters/applications/prometheus_health_check_service.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class PrometheusHealthCheckService
- include Gitlab::Utils::StrongMemoize
- include Gitlab::Routing
-
- def initialize(cluster)
- @cluster = cluster
- @logger = Gitlab::AppJsonLogger.build
- end
-
- def execute
- raise 'Invalid cluster type. Only project types are allowed.' unless @cluster.project_type?
-
- return unless prometheus_application.installed?
-
- project = @cluster.clusterable
-
- @logger.info(
- message: 'Prometheus health check',
- cluster_id: @cluster.id,
- newly_unhealthy: became_unhealthy?,
- currently_healthy: currently_healthy?,
- was_healthy: was_healthy?
- )
-
- send_notification(project) if became_unhealthy?
-
- prometheus_application.update_columns(healthy: currently_healthy?) if health_changed?
- end
-
- private
-
- def prometheus_application
- strong_memoize(:prometheus_application) do
- @cluster.application_prometheus
- end
- end
-
- def currently_healthy?
- strong_memoize(:currently_healthy) do
- prometheus_application.prometheus_client.healthy?
- end
- end
-
- def became_unhealthy?
- strong_memoize(:became_unhealthy) do
- (was_healthy? || was_healthy?.nil?) && !currently_healthy?
- end
- end
-
- def was_healthy?
- strong_memoize(:was_healthy) do
- prometheus_application.healthy
- end
- end
-
- def health_changed?
- was_healthy? != currently_healthy?
- end
-
- def send_notification(project)
- notification_payload = build_notification_payload(project)
- integration = project.alert_management_http_integrations.active.first
-
- Projects::Alerting::NotifyService.new(project, notification_payload).execute(integration&.token, integration)
-
- @logger.info(message: 'Successfully notified of Prometheus newly unhealthy', cluster_id: @cluster.id, project_id: project.id)
- end
-
- def build_notification_payload(project)
- cluster_path = namespace_project_cluster_path(
- project_id: project.path,
- namespace_id: project.namespace.path,
- id: @cluster.id
- )
-
- {
- title: "Prometheus is Unhealthy. Cluster Name: #{@cluster.name}",
- description: "Prometheus is unhealthy for the cluster: [#{@cluster.name}](#{cluster_path}) attached to project #{project.name}."
- }
- end
- end
- end
-end
diff --git a/app/services/clusters/cleanup/project_namespace_service.rb b/app/services/clusters/cleanup/project_namespace_service.rb
index 16254041306..0173f93f625 100644
--- a/app/services/clusters/cleanup/project_namespace_service.rb
+++ b/app/services/clusters/cleanup/project_namespace_service.rb
@@ -35,9 +35,11 @@ module Clusters
end
def kubeclient_delete_namespace(kubernetes_namespace)
- cluster.kubeclient.delete_namespace(kubernetes_namespace.namespace)
+ cluster.kubeclient&.delete_namespace(kubernetes_namespace.namespace)
rescue Kubeclient::ResourceNotFoundError
- # no-op: nothing to delete
+ # The resources have already been deleted, possibly on a previous attempt that timed out
+ rescue Gitlab::UrlBlocker::BlockedUrlError
+ # User gave an invalid cluster from the start, or deleted the endpoint before this job ran
end
end
end
diff --git a/app/services/clusters/cleanup/service_account_service.rb b/app/services/clusters/cleanup/service_account_service.rb
index baac9e4a9e7..53f968cd409 100644
--- a/app/services/clusters/cleanup/service_account_service.rb
+++ b/app/services/clusters/cleanup/service_account_service.rb
@@ -16,11 +16,14 @@ module Clusters
def delete_gitlab_service_account
log_event(:deleting_gitlab_service_account)
- cluster.kubeclient.delete_service_account(
+ cluster.kubeclient&.delete_service_account(
::Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAME,
::Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE
)
rescue Kubeclient::ResourceNotFoundError
+ # The resources have already been deleted, possibly on a previous attempt that timed out
+ rescue Gitlab::UrlBlocker::BlockedUrlError
+ # User gave an invalid cluster from the start, or deleted the endpoint before this job ran
end
end
end
diff --git a/app/services/clusters/integrations/prometheus_health_check_service.rb b/app/services/clusters/integrations/prometheus_health_check_service.rb
new file mode 100644
index 00000000000..cd06e59449c
--- /dev/null
+++ b/app/services/clusters/integrations/prometheus_health_check_service.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Integrations
+ class PrometheusHealthCheckService
+ include Gitlab::Utils::StrongMemoize
+ include Gitlab::Routing
+
+ def initialize(cluster)
+ @cluster = cluster
+ @logger = Gitlab::AppJsonLogger.build
+ end
+
+ def execute
+ raise 'Invalid cluster type. Only project types are allowed.' unless @cluster.project_type?
+
+ return unless prometheus_integration.enabled
+
+ project = @cluster.clusterable
+
+ @logger.info(
+ message: 'Prometheus health check',
+ cluster_id: @cluster.id,
+ newly_unhealthy: became_unhealthy?,
+ currently_healthy: currently_healthy?,
+ was_healthy: was_healthy?
+ )
+
+ send_notification(project) if became_unhealthy?
+
+ prometheus_integration.update_columns(health_status: current_health_status) if health_changed?
+ end
+
+ private
+
+ def prometheus_integration
+ strong_memoize(:prometheus_integration) do
+ @cluster.integration_prometheus
+ end
+ end
+
+ def current_health_status
+ if currently_healthy?
+ :healthy
+ else
+ :unhealthy
+ end
+ end
+
+ def currently_healthy?
+ strong_memoize(:currently_healthy) do
+ prometheus_integration.prometheus_client.healthy?
+ end
+ end
+
+ def became_unhealthy?
+ strong_memoize(:became_unhealthy) do
+ (was_healthy? || was_unknown?) && !currently_healthy?
+ end
+ end
+
+ def was_healthy?
+ strong_memoize(:was_healthy) do
+ prometheus_integration.healthy?
+ end
+ end
+
+ def was_unknown?
+ strong_memoize(:was_unknown) do
+ prometheus_integration.unknown?
+ end
+ end
+
+ def health_changed?
+ was_healthy? != currently_healthy?
+ end
+
+ def send_notification(project)
+ notification_payload = build_notification_payload(project)
+ integration = project.alert_management_http_integrations.active.first
+
+ Projects::Alerting::NotifyService.new(project, notification_payload).execute(integration&.token, integration)
+
+ @logger.info(message: 'Successfully notified of Prometheus newly unhealthy', cluster_id: @cluster.id, project_id: project.id)
+ end
+
+ def build_notification_payload(project)
+ cluster_path = namespace_project_cluster_path(
+ project_id: project.path,
+ namespace_id: project.namespace.path,
+ id: @cluster.id
+ )
+
+ {
+ title: "Prometheus is Unhealthy. Cluster Name: #{@cluster.name}",
+ description: "Prometheus is unhealthy for the cluster: [#{@cluster.name}](#{cluster_path}) attached to project #{project.name}."
+ }
+ end
+ end
+ end
+end
diff --git a/app/services/concerns/alert_management/responses.rb b/app/services/concerns/alert_management/responses.rb
new file mode 100644
index 00000000000..183a831a00a
--- /dev/null
+++ b/app/services/concerns/alert_management/responses.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ # Module to hold common response logic for AlertManagement services.
+ module Responses
+ def success(alerts)
+ ServiceResponse.success(payload: { alerts: Array(alerts) })
+ end
+
+ def bad_request
+ ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
+ end
+
+ def unauthorized
+ ServiceResponse.error(message: 'Unauthorized', http_status: :unauthorized)
+ end
+
+ def unprocessable_entity
+ ServiceResponse.error(message: 'Unprocessable Entity', http_status: :unprocessable_entity)
+ end
+
+ def forbidden
+ ServiceResponse.error(message: 'Forbidden', http_status: :forbidden)
+ end
+ end
+end
diff --git a/app/services/concerns/issues/issue_type_helpers.rb b/app/services/concerns/issues/issue_type_helpers.rb
new file mode 100644
index 00000000000..44c20d20ff1
--- /dev/null
+++ b/app/services/concerns/issues/issue_type_helpers.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Issues
+ module IssueTypeHelpers
+ # @param object [Issue, Project]
+ # @param issue_type [String, Symbol]
+ def create_issue_type_allowed?(object, issue_type)
+ WorkItem::Type.base_types.key?(issue_type.to_s) &&
+ can?(current_user, :"create_#{issue_type}", object)
+ end
+ end
+end
diff --git a/app/services/concerns/members/bulk_create_users.rb b/app/services/concerns/members/bulk_create_users.rb
index 4498f40c396..b98917f1396 100644
--- a/app/services/concerns/members/bulk_create_users.rb
+++ b/app/services/concerns/members/bulk_create_users.rb
@@ -6,7 +6,7 @@ module Members
included do
class << self
- def add_users(source, users, access_level, current_user: nil, expires_at: nil)
+ def add_users(source, users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
return [] unless users.present?
emails, users, existing_members = parse_users_list(source, users)
@@ -18,7 +18,9 @@ module Members
access_level,
existing_members: existing_members,
current_user: current_user,
- expires_at: expires_at)
+ expires_at: expires_at,
+ tasks_to_be_done: tasks_to_be_done,
+ tasks_project_id: tasks_project_id)
.execute
end
end
diff --git a/app/services/customer_relations/contacts/base_service.rb b/app/services/customer_relations/contacts/base_service.rb
index 89f6f2c3f1f..1797e5021a1 100644
--- a/app/services/customer_relations/contacts/base_service.rb
+++ b/app/services/customer_relations/contacts/base_service.rb
@@ -6,7 +6,7 @@ module CustomerRelations
private
def allowed?
- current_user&.can?(:admin_contact, group)
+ current_user&.can?(:admin_crm_contact, group)
end
def error(message)
diff --git a/app/services/customer_relations/organizations/base_service.rb b/app/services/customer_relations/organizations/base_service.rb
index 8f8480d697c..9b4ccafcea6 100644
--- a/app/services/customer_relations/organizations/base_service.rb
+++ b/app/services/customer_relations/organizations/base_service.rb
@@ -6,7 +6,7 @@ module CustomerRelations
private
def allowed?
- current_user&.can?(:admin_organization, group)
+ current_user&.can?(:admin_crm_organization, group)
end
def error(message)
diff --git a/app/services/dependency_proxy/find_or_create_blob_service.rb b/app/services/dependency_proxy/find_or_create_blob_service.rb
index 0a6db6e3d34..1b43263a3ba 100644
--- a/app/services/dependency_proxy/find_or_create_blob_service.rb
+++ b/app/services/dependency_proxy/find_or_create_blob_service.rb
@@ -30,8 +30,7 @@ module DependencyProxy
blob.save!
end
- # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
- blob.touch if from_cache
+ blob.read! if from_cache
success(blob: blob, from_cache: from_cache)
end
diff --git a/app/services/dependency_proxy/find_or_create_manifest_service.rb b/app/services/dependency_proxy/find_or_create_manifest_service.rb
index 1976d4d47f4..aeb62be9f3a 100644
--- a/app/services/dependency_proxy/find_or_create_manifest_service.rb
+++ b/app/services/dependency_proxy/find_or_create_manifest_service.rb
@@ -14,18 +14,18 @@ module DependencyProxy
def execute
@manifest = @group.dependency_proxy_manifests
.active
- .find_or_initialize_by_file_name_or_digest(file_name: @file_name, digest: @tag)
+ .find_by_file_name_or_digest(file_name: @file_name, digest: @tag)
head_result = DependencyProxy::HeadManifestService.new(@image, @tag, @token).execute
- if cached_manifest_matches?(head_result)
- @manifest.touch
+ return respond if cached_manifest_matches?(head_result)
- return success(manifest: @manifest, from_cache: true)
+ if Feature.enabled?(:dependency_proxy_manifest_workhorse, @group, default_enabled: :yaml)
+ success(manifest: nil, from_cache: false)
+ else
+ pull_new_manifest
+ respond(from_cache: false)
end
-
- pull_new_manifest
- respond(from_cache: false)
rescue Timeout::Error, *Gitlab::HTTP::HTTP_ERRORS
respond
end
@@ -34,12 +34,19 @@ module DependencyProxy
def pull_new_manifest
DependencyProxy::PullManifestService.new(@image, @tag, @token).execute_with_manifest do |new_manifest|
- @manifest.update!(
+ params = {
+ file_name: @file_name,
content_type: new_manifest[:content_type],
digest: new_manifest[:digest],
file: new_manifest[:file],
size: new_manifest[:file].size
- )
+ }
+
+ if @manifest
+ @manifest.update!(params)
+ else
+ @manifest = @group.dependency_proxy_manifests.create!(params)
+ end
end
end
@@ -50,9 +57,8 @@ module DependencyProxy
end
def respond(from_cache: true)
- if @manifest.persisted?
- # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
- @manifest.touch if from_cache
+ if @manifest
+ @manifest.read!
success(manifest: @manifest, from_cache: from_cache)
else
diff --git a/app/services/dependency_proxy/head_manifest_service.rb b/app/services/dependency_proxy/head_manifest_service.rb
index ecc3eb77399..cd575b83a98 100644
--- a/app/services/dependency_proxy/head_manifest_service.rb
+++ b/app/services/dependency_proxy/head_manifest_service.rb
@@ -14,7 +14,10 @@ module DependencyProxy
response = Gitlab::HTTP.head(manifest_url, headers: auth_headers.merge(Accept: ACCEPT_HEADERS))
if response.success?
- success(digest: response.headers['docker-content-digest'], content_type: response.headers['content-type'])
+ success(
+ digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER],
+ content_type: response.headers['content-type']
+ )
else
error(response.body, response.code)
end
diff --git a/app/services/dependency_proxy/pull_manifest_service.rb b/app/services/dependency_proxy/pull_manifest_service.rb
index 31494773cc0..e8f0ad6374a 100644
--- a/app/services/dependency_proxy/pull_manifest_service.rb
+++ b/app/services/dependency_proxy/pull_manifest_service.rb
@@ -20,7 +20,13 @@ module DependencyProxy
file.write(response.body)
file.flush
- yield(success(file: file, digest: response.headers['docker-content-digest'], content_type: response.headers['content-type']))
+ yield(
+ success(
+ file: file,
+ digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER],
+ content_type: response.headers['content-type']
+ )
+ )
ensure
file.close
file.unlink
diff --git a/app/services/deployments/archive_in_project_service.rb b/app/services/deployments/archive_in_project_service.rb
new file mode 100644
index 00000000000..a593721f390
--- /dev/null
+++ b/app/services/deployments/archive_in_project_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Deployments
+ # This service archives old deploymets and deletes deployment refs for
+ # keeping the project repository performant.
+ class ArchiveInProjectService < ::BaseService
+ BATCH_SIZE = 100
+
+ def execute
+ unless ::Feature.enabled?(:deployments_archive, project, default_enabled: :yaml)
+ return error('Feature flag is not enabled')
+ end
+
+ deployments = Deployment.archivables_in(project, limit: BATCH_SIZE)
+
+ return success(result: :empty) if deployments.empty?
+
+ ids = deployments.map(&:id)
+ ref_paths = deployments.map(&:ref_path)
+
+ project.repository.delete_refs(*ref_paths)
+ project.deployments.id_in(ids).update_all(archived: true)
+
+ success(result: :archived, count: ids.count)
+ end
+ end
+end
diff --git a/app/services/deployments/link_merge_requests_service.rb b/app/services/deployments/link_merge_requests_service.rb
index 39fbef5dee2..40385418e48 100644
--- a/app/services/deployments/link_merge_requests_service.rb
+++ b/app/services/deployments/link_merge_requests_service.rb
@@ -16,7 +16,7 @@ module Deployments
# Review apps have the environment type set (e.g. to `review`, though the
# exact value may differ). We don't want to link merge requests to review
# app deployments, as this is not useful.
- return if deployment.environment.environment_type
+ return unless deployment.environment.should_link_to_merge_requests?
# This service is triggered by a Sidekiq worker, which only runs when a
# deployment is successful. We add an extra check here in case we ever
diff --git a/app/services/design_management/copy_design_collection/copy_service.rb b/app/services/design_management/copy_design_collection/copy_service.rb
index c43696442d2..5e557e9ea53 100644
--- a/app/services/design_management/copy_design_collection/copy_service.rb
+++ b/app/services/design_management/copy_design_collection/copy_service.rb
@@ -181,12 +181,12 @@ module DesignManagement
)
end
- # TODO Replace `Gitlab::Database.main.bulk_insert` with `BulkInsertSafe`
+ # TODO Replace `ApplicationRecord.legacy_bulk_insert` with `BulkInsertSafe`
# once https://gitlab.com/gitlab-org/gitlab/-/issues/247718 is fixed.
# When this is fixed, we can remove the call to
# `with_project_iid_supply` above, since the objects will be instantiated
# and callbacks (including `ensure_project_iid!`) will fire.
- ::Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ::ApplicationRecord.legacy_bulk_insert( # rubocop:disable Gitlab/BulkInsert
DesignManagement::Design.table_name,
new_rows,
return_ids: true
@@ -207,9 +207,9 @@ module DesignManagement
)
end
- # TODO Replace `Gitlab::Database.main.bulk_insert` with `BulkInsertSafe`
+ # TODO Replace `ApplicationRecord.legacy_bulk_insert` with `BulkInsertSafe`
# once https://gitlab.com/gitlab-org/gitlab/-/issues/247718 is fixed.
- ::Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ::ApplicationRecord.legacy_bulk_insert( # rubocop:disable Gitlab/BulkInsert
DesignManagement::Version.table_name,
new_rows,
return_ids: true
@@ -239,7 +239,7 @@ module DesignManagement
end
# We cannot use `BulkInsertSafe` because of the uploader mounted in `Action`.
- ::Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ::ApplicationRecord.legacy_bulk_insert( # rubocop:disable Gitlab/BulkInsert
DesignManagement::Action.table_name,
new_rows
)
@@ -278,7 +278,7 @@ module DesignManagement
# We cannot use `BulkInsertSafe` due to the LfsObjectsProject#update_project_statistics
# callback that fires after_commit.
- ::Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ::ApplicationRecord.legacy_bulk_insert( # rubocop:disable Gitlab/BulkInsert
LfsObjectsProject.table_name,
new_rows,
on_conflict: :do_nothing # Upsert
diff --git a/app/services/emails/destroy_service.rb b/app/services/emails/destroy_service.rb
index d10833e66cb..d211c3470b2 100644
--- a/app/services/emails/destroy_service.rb
+++ b/app/services/emails/destroy_service.rb
@@ -3,6 +3,8 @@
module Emails
class DestroyService < ::Emails::BaseService
def execute(email)
+ raise StandardError, 'Cannot delete primary email' if email.user_primary_email?
+
email.destroy && update_secondary_emails!(email.email)
end
diff --git a/app/services/error_tracking/collect_error_service.rb b/app/services/error_tracking/collect_error_service.rb
index 477453a693e..304e3898ee5 100644
--- a/app/services/error_tracking/collect_error_service.rb
+++ b/app/services/error_tracking/collect_error_service.rb
@@ -15,7 +15,7 @@ module ErrorTracking
)
# The payload field contains all the data on error including stacktrace in jsonb.
- # Together with occured_at these are 2 main attributes that we need to save here.
+ # Together with occurred_at these are 2 main attributes that we need to save here.
error.events.create!(
environment: event['environment'],
description: exception['value'],
@@ -28,7 +28,18 @@ module ErrorTracking
private
def event
- params[:event]
+ @event ||= format_event(params[:event])
+ end
+
+ def format_event(event)
+ # Some SDK send exception payload as Array. For exmple Go lang SDK.
+ # We need to convert it to hash format we expect.
+ if event['exception'].is_a?(Array)
+ exception = event['exception']
+ event['exception'] = { 'values' => exception }
+ end
+
+ event
end
def exception
diff --git a/app/services/google_cloud/service_accounts_service.rb b/app/services/google_cloud/service_accounts_service.rb
new file mode 100644
index 00000000000..29ed69693b0
--- /dev/null
+++ b/app/services/google_cloud/service_accounts_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module GoogleCloud
+ ##
+ # GCP keys used to store Google Cloud Service Accounts
+ GCP_KEYS = %w[GCP_PROJECT_ID GCP_SERVICE_ACCOUNT GCP_SERVICE_ACCOUNT_KEY].freeze
+
+ ##
+ # This service deals with GCP Service Accounts in GitLab
+
+ class ServiceAccountsService < ::BaseService
+ ##
+ # Find GCP Service Accounts in a GitLab project
+ #
+ # This method looks up GitLab project's CI vars
+ # and returns Google Cloud Service Accounts combinations
+ # aligning GitLab project and environment to GCP projects
+
+ def find_for_project
+ group_vars_by_environment.map do |environment_scope, value|
+ {
+ environment: environment_scope,
+ gcp_project: value['GCP_PROJECT_ID'],
+ service_account_exists: value['GCP_SERVICE_ACCOUNT'].present?,
+ service_account_key_exists: value['GCP_SERVICE_ACCOUNT_KEY'].present?
+ }
+ end
+ end
+
+ private
+
+ def group_vars_by_environment
+ filtered_vars = @project.variables.filter { |variable| GCP_KEYS.include? variable.key }
+ filtered_vars.each_with_object({}) do |variable, grouped|
+ grouped[variable.environment_scope] ||= {}
+ grouped[variable.environment_scope][variable.key] = variable.value
+ end
+ end
+ end
+end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index f900927793a..da3cebc2e6d 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -6,6 +6,7 @@ module Groups
@current_user = user
@params = params.dup
@chat_team = @params.delete(:create_chat_team)
+ @create_event = @params.delete(:create_event)
end
def execute
@@ -42,15 +43,26 @@ module Groups
end
end
+ after_create_hook
+
@group
end
private
+ attr_reader :create_event
+
def after_build_hook(group, params)
# overridden in EE
end
+ def after_create_hook
+ if group.persisted? && group.root?
+ delay = Namespaces::InviteTeamEmailService::DELIVERY_DELAY_IN_MINUTES
+ Namespaces::InviteTeamEmailWorker.perform_in(delay, group.id, current_user.id)
+ end
+ end
+
def remove_unallowed_params
params.delete(:default_branch_protection) unless can?(current_user, :create_group_with_default_branch_protection)
params.delete(:allow_mfa_for_subgroups)
diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb
index f9db552f743..c8c2124078d 100644
--- a/app/services/groups/import_export/import_service.rb
+++ b/app/services/groups/import_export/import_service.rb
@@ -14,7 +14,7 @@ module Groups
def async_execute
group_import_state = GroupImportState.safe_find_or_create_by!(group: group, user: current_user)
- jid = GroupImportWorker.perform_async(current_user.id, group.id)
+ jid = GroupImportWorker.with_status.perform_async(current_user.id, group.id)
if jid.present?
group_import_state.update!(jid: jid)
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index 334083a859f..cd89eb799dc 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -175,21 +175,18 @@ module Groups
end
def refresh_project_authorizations
- ProjectAuthorization.where(project_id: @group.all_projects.select(:id)).delete_all # rubocop: disable CodeReuse/ActiveRecord
+ projects_to_update = Set.new
- # refresh authorized projects for current_user immediately
- current_user.refresh_authorized_projects
-
- # schedule refreshing projects for all the members of the group
- @group.refresh_members_authorized_projects
+ # All projects in this hierarchy need to have their project authorizations recalculated
+ @group.all_projects.each_batch { |prjs| projects_to_update.merge(prjs.ids) } # rubocop: disable CodeReuse/ActiveRecord
# When a group is transferred, it also affects who gets access to the projects shared to
# the subgroups within its hierarchy, so we also schedule jobs that refresh authorizations for all such shared projects.
- project_group_shares_within_the_hierarchy = ProjectGroupLink.in_group(group.self_and_descendants.select(:id))
-
- project_group_shares_within_the_hierarchy.find_each do |project_group_link|
- AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project_group_link.project_id)
+ ProjectGroupLink.in_group(@group.self_and_descendants.select(:id)).each_batch do |project_group_links|
+ projects_to_update.merge(project_group_links.pluck(:project_id)) # rubocop: disable CodeReuse/ActiveRecord
end
+
+ AuthorizedProjectUpdate::ProjectAccessChangedService.new(projects_to_update.to_a).execute unless projects_to_update.empty?
end
def raise_transfer_error(message)
@@ -199,7 +196,7 @@ module Groups
def localized_error_messages
{
database_not_supported: s_('TransferGroup|Database is not supported.'),
- namespace_with_same_path: s_('TransferGroup|The parent group already has a subgroup with the same path.'),
+ namespace_with_same_path: s_('TransferGroup|The parent group already has a subgroup or a project with the same path.'),
group_is_already_root: s_('TransferGroup|Group is already a root group.'),
same_parent_as_current: s_('TransferGroup|Group is already associated to the parent group.'),
invalid_policies: s_("TransferGroup|You don't have enough permissions."),
diff --git a/app/services/import/github/notes/create_service.rb b/app/services/import/github/notes/create_service.rb
new file mode 100644
index 00000000000..79145f42313
--- /dev/null
+++ b/app/services/import/github/notes/create_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Import
+ module Github
+ module Notes
+ class CreateService < ::Notes::CreateService
+ # Github does not have support to quick actions in notes (like /assign)
+ # Therefore, when importing notes we skip the quick actions processing
+ def quick_actions_supported?(_note)
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb
index d8b639bb422..279d3051848 100644
--- a/app/services/issuable/clone/attributes_rewriter.rb
+++ b/app/services/issuable/clone/attributes_rewriter.rb
@@ -99,7 +99,7 @@ module Issuable
yield(event)
end.compact
- Gitlab::Database.main.bulk_insert(table_name, events) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(table_name, events) # rubocop:disable Gitlab/BulkInsert
end
end
diff --git a/app/services/issuable_links/list_service.rb b/app/services/issuable_links/list_service.rb
index fe9678dcc32..cc41a65379a 100644
--- a/app/services/issuable_links/list_service.rb
+++ b/app/services/issuable_links/list_service.rb
@@ -12,11 +12,16 @@ module IssuableLinks
end
def execute
- serializer.new(current_user: current_user, issuable: issuable).represent(child_issuables)
+ serializer.new(current_user: current_user, issuable: issuable)
+ .represent(child_issuables, serializer_options)
end
private
+ def serializer_options
+ {}
+ end
+
def serializer
raise NotImplementedError
end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 6dce9fd6e73..efb5de5b17c 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -3,6 +3,7 @@
module Issues
class BaseService < ::IssuableBaseService
include IncidentManagement::UsageData
+ include IssueTypeHelpers
def hook_data(issue, action, old_associations: {})
hook_data = issue.to_hook_data(current_user, old_associations: old_associations)
@@ -44,7 +45,7 @@ module Issues
def filter_params(issue)
super
- params.delete(:issue_type) unless issue_type_allowed?(issue)
+ params.delete(:issue_type) unless create_issue_type_allowed?(issue, params[:issue_type])
filter_incident_label(issue) if params[:issue_type]
moved_issue = params.delete(:moved_issue)
@@ -89,12 +90,6 @@ module Issues
Milestones::IssuesCountService.new(milestone).delete_cache
end
- # @param object [Issue, Project]
- def issue_type_allowed?(object)
- WorkItem::Type.base_types.key?(params[:issue_type]) &&
- can?(current_user, :"create_#{params[:issue_type]}", object)
- end
-
# @param issue [Issue]
def filter_incident_label(issue)
return unless add_incident_label?(issue) || remove_incident_label?(issue)
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index 7fdc8daf15c..8fd844c4886 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -80,7 +80,7 @@ module Issues
]
allowed_params << :milestone_id if can?(current_user, :admin_issue, project)
- allowed_params << :issue_type if issue_type_allowed?(project)
+ allowed_params << :issue_type if create_issue_type_allowed?(project, params[:issue_type])
params.slice(*allowed_params)
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index ac846c769a3..65f143d0b21 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -62,6 +62,7 @@ module Issues
def perform_incident_management_actions(issue)
resolve_alert(issue)
+ resolve_incident(issue)
end
def close_external_issue(issue, closed_via)
@@ -91,6 +92,14 @@ module Issues
end
end
+ def resolve_incident(issue)
+ return unless issue.incident?
+
+ status = issue.incident_management_issuable_escalation_status || issue.build_incident_management_issuable_escalation_status
+
+ SystemNoteService.resolve_incident_status(issue, current_user) if status.resolve
+ end
+
def store_first_mentioned_in_commit_at(issue, merge_request, max_commit_lookup: 100)
metrics = issue.metrics
return if metrics.nil? || metrics.first_mentioned_in_commit_at
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index fcedd1c1c8d..fa8d380404b 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -6,7 +6,7 @@ module Issues
prepend RateLimitedService
rate_limit key: :issues_create,
- opts: { scope: [:project, :current_user], users_allowlist: -> { [User.support_bot.username] } }
+ opts: { scope: [:project, :current_user, :external_author] }
# NOTE: For Issues::CreateService, we require the spam_params and do not default it to nil, because
# spam_checking is likely to be necessary. However, if there is not a request available in scope
@@ -25,6 +25,10 @@ module Issues
create(@issue, skip_system_notes: skip_system_notes)
end
+ def external_author
+ params[:external_author] # present when creating an issue using service desk (email: from)
+ end
+
def before_create(issue)
Spam::SpamActionService.new(
spammable: issue,
diff --git a/app/services/issues/set_crm_contacts_service.rb b/app/services/issues/set_crm_contacts_service.rb
new file mode 100644
index 00000000000..13fe30b5ac8
--- /dev/null
+++ b/app/services/issues/set_crm_contacts_service.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+module Issues
+ class SetCrmContactsService < ::BaseProjectService
+ attr_accessor :issue, :errors
+
+ MAX_ADDITIONAL_CONTACTS = 6
+
+ def execute(issue)
+ @issue = issue
+ @errors = []
+
+ return error_no_permissions unless allowed?
+ return error_invalid_params unless valid_params?
+
+ determine_changes if params[:crm_contact_ids]
+
+ return error_too_many if too_many?
+
+ add_contacts if params[:add_crm_contact_ids]
+ remove_contacts if params[:remove_crm_contact_ids]
+
+ if issue.valid?
+ ServiceResponse.success(payload: issue)
+ else
+ # The default error isn't very helpful: "Issue customer relations contacts is invalid"
+ issue.errors.delete(:issue_customer_relations_contacts)
+ issue.errors.add(:issue_customer_relations_contacts, errors.to_sentence)
+ ServiceResponse.error(payload: issue, message: issue.errors.full_messages)
+ end
+ end
+
+ private
+
+ def determine_changes
+ existing_contact_ids = issue.issue_customer_relations_contacts.map(&:contact_id)
+ params[:add_crm_contact_ids] = params[:crm_contact_ids] - existing_contact_ids
+ params[:remove_crm_contact_ids] = existing_contact_ids - params[:crm_contact_ids]
+ end
+
+ def add_contacts
+ params[:add_crm_contact_ids].uniq.each do |contact_id|
+ issue_contact = issue.issue_customer_relations_contacts.create(contact_id: contact_id)
+
+ unless issue_contact.persisted?
+ # The validation ensures that the id exists and the user has permission
+ errors << "#{contact_id}: The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ end
+ end
+ end
+
+ def remove_contacts
+ issue.issue_customer_relations_contacts
+ .where(contact_id: params[:remove_crm_contact_ids]) # rubocop: disable CodeReuse/ActiveRecord
+ .delete_all
+ end
+
+ def allowed?
+ current_user&.can?(:set_issue_crm_contacts, issue)
+ end
+
+ def valid_params?
+ set_present? ^ add_or_remove_present?
+ end
+
+ def set_present?
+ params[:crm_contact_ids].present?
+ end
+
+ def add_or_remove_present?
+ params[:add_crm_contact_ids].present? || params[:remove_crm_contact_ids].present?
+ end
+
+ def too_many?
+ params[:add_crm_contact_ids] && params[:add_crm_contact_ids].length > MAX_ADDITIONAL_CONTACTS
+ end
+
+ def error_no_permissions
+ ServiceResponse.error(message: ['You have insufficient permissions to set customer relations contacts for this issue'])
+ end
+
+ def error_invalid_params
+ ServiceResponse.error(message: ['You cannot combine crm_contact_ids with add_crm_contact_ids or remove_crm_contact_ids'])
+ end
+
+ def error_too_many
+ ServiceResponse.error(payload: issue, message: ["You can only add up to #{MAX_ADDITIONAL_CONTACTS} contacts at one time"])
+ end
+ end
+end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index d120b007af2..824a609dfb9 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -80,7 +80,7 @@ module Issues
todo_service.reassigned_assignable(issue, current_user, old_assignees)
track_incident_action(current_user, issue, :incident_assigned)
- if Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:broadcast_issue_updates, issue.project)
+ if Feature.enabled?(:broadcast_issue_updates, issue.project, default_enabled: :yaml)
GraphqlTriggers.issuable_assignees_updated(issue)
end
end
diff --git a/app/services/jira/requests/base.rb b/app/services/jira/requests/base.rb
index 56484075d08..a16f8bbd367 100644
--- a/app/services/jira/requests/base.rb
+++ b/app/services/jira/requests/base.rb
@@ -67,9 +67,19 @@ module Jira
ServiceResponse.error(message: error_message(e))
end
+ def auth_docs_link_start
+ auth_docs_link_url = Rails.application.routes.url_helpers.help_page_path('integration/jira', anchor: 'authentication-in-jira')
+ '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auth_docs_link_url }
+ end
+
+ def config_docs_link_start
+ config_docs_link_url = Rails.application.routes.url_helpers.help_page_path('integration/jira/configure')
+ '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: config_docs_link_url }
+ end
+
def error_message(error)
reportable_error_message(error) ||
- s_('JiraRequest|An error occurred while requesting data from Jira. Check your Jira integration configuration and try again.')
+ s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
end
# Returns a user-facing error message if possible, otherwise `nil`.
@@ -93,11 +103,11 @@ module Jira
def reportable_jira_ruby_error_message(error)
case error.message
when 'Unauthorized'
- s_('JiraRequest|The credentials for accessing Jira are not valid. Check your Jira integration credentials and try again.')
+ s_('JiraRequest|The credentials for accessing Jira are not valid. Check your %{docs_link_start}Jira integration credentials%{docs_link_end} and try again.').html_safe % { docs_link_start: auth_docs_link_start, docs_link_end: '</a>'.html_safe }
when 'Forbidden'
- s_('JiraRequest|The credentials for accessing Jira are not allowed to access the data. Check your Jira integration credentials and try again.')
+ s_('JiraRequest|The credentials for accessing Jira are not allowed to access the data. Check your %{docs_link_start}Jira integration credentials%{docs_link_end} and try again.').html_safe % { docs_link_start: auth_docs_link_start, docs_link_end: '</a>'.html_safe }
when 'Bad Request'
- s_('JiraRequest|An error occurred while requesting data from Jira. Check your Jira integration configuration and try again.')
+ s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
when /errorMessages/
jira_ruby_json_error_message(error.message)
end
@@ -111,7 +121,7 @@ module Jira
messages = Rails::Html::FullSanitizer.new.sanitize(messages).presence
return unless messages
- s_('JiraRequest|An error occurred while requesting data from Jira: %{messages}. Check your Jira integration configuration and try again.') % { messages: messages }
+ s_('JiraRequest|An error occurred while requesting data from Jira: %{messages}. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { messages: messages, docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
rescue JSON::ParserError
end
end
diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb
index a05090d6bfb..19d419609a5 100644
--- a/app/services/labels/transfer_service.rb
+++ b/app/services/labels/transfer_service.rb
@@ -50,21 +50,32 @@ module Labels
# rubocop: disable CodeReuse/ActiveRecord
def group_labels_applied_to_issues
- @group_labels_applied_to_issues ||= Label.joins(:issues)
- .where(
- issues: { project_id: project.id },
- labels: { group_id: old_group.self_and_ancestors }
- )
+ @labels_applied_to_issues ||= if use_optimized_group_labels_query?
+ Label.joins(:issues)
+ .joins("INNER JOIN namespaces on namespaces.id = labels.group_id AND namespaces.type = 'Group'" )
+ .where(issues: { project_id: project.id }).reorder(nil)
+ else
+ Label.joins(:issues).where(
+ issues: { project_id: project.id },
+ labels: { group_id: old_group.self_and_ancestors }
+ )
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def group_labels_applied_to_merge_requests
- @group_labels_applied_to_merge_requests ||= Label.joins(:merge_requests)
- .where(
- merge_requests: { target_project_id: project.id },
- labels: { group_id: old_group.self_and_ancestors }
- )
+ @labels_applied_to_mrs ||= if use_optimized_group_labels_query?
+ Label.joins(:merge_requests)
+ .joins("INNER JOIN namespaces on namespaces.id = labels.group_id AND namespaces.type = 'Group'" )
+ .where(merge_requests: { target_project_id: project.id }).reorder(nil)
+ else
+ Label.joins(:merge_requests)
+ .where(
+ merge_requests: { target_project_id: project.id },
+ labels: { group_id: old_group.self_and_ancestors }
+ )
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -88,5 +99,9 @@ module Labels
.update_all(label_id: new_label_id)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def use_optimized_group_labels_query?
+ Feature.enabled?(:use_optimized_group_labels_query, project.root_namespace, default_enabled: :yaml)
+ end
end
end
diff --git a/app/services/loose_foreign_keys/batch_cleaner_service.rb b/app/services/loose_foreign_keys/batch_cleaner_service.rb
new file mode 100644
index 00000000000..06c05e8ff54
--- /dev/null
+++ b/app/services/loose_foreign_keys/batch_cleaner_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module LooseForeignKeys
+ class BatchCleanerService
+ def initialize(parent_klass:, deleted_parent_records:, modification_tracker: LooseForeignKeys::ModificationTracker.new, models_by_table_name:)
+ @parent_klass = parent_klass
+ @deleted_parent_records = deleted_parent_records
+ @modification_tracker = modification_tracker
+ @models_by_table_name = models_by_table_name
+ @deleted_records_counter = Gitlab::Metrics.counter(
+ :loose_foreign_key_processed_deleted_records,
+ 'The number of processed loose foreign key deleted records'
+ )
+ end
+
+ def execute
+ parent_klass.loose_foreign_key_definitions.each do |foreign_key_definition|
+ run_cleaner_service(foreign_key_definition, with_skip_locked: true)
+ break if modification_tracker.over_limit?
+
+ run_cleaner_service(foreign_key_definition, with_skip_locked: false)
+ break if modification_tracker.over_limit?
+ end
+
+ return if modification_tracker.over_limit?
+
+ # At this point, all associations are cleaned up, we can update the status of the parent records
+ update_count = LooseForeignKeys::DeletedRecord.mark_records_processed(deleted_parent_records)
+
+ deleted_records_counter.increment({ table: parent_klass.table_name, db_config_name: LooseForeignKeys::DeletedRecord.connection.pool.db_config.name }, update_count)
+ end
+
+ private
+
+ attr_reader :parent_klass, :deleted_parent_records, :modification_tracker, :models_by_table_name, :deleted_records_counter
+
+ def record_result(cleaner, result)
+ if cleaner.async_delete?
+ modification_tracker.add_deletions(result[:table], result[:affected_rows])
+ elsif cleaner.async_nullify?
+ modification_tracker.add_updates(result[:table], result[:affected_rows])
+ end
+ end
+
+ def run_cleaner_service(foreign_key_definition, with_skip_locked:)
+ cleaner = CleanerService.new(
+ model: models_by_table_name.fetch(foreign_key_definition.to_table),
+ foreign_key_definition: foreign_key_definition,
+ deleted_parent_records: deleted_parent_records,
+ with_skip_locked: with_skip_locked
+ )
+
+ loop do
+ result = cleaner.execute
+ record_result(cleaner, result)
+
+ break if modification_tracker.over_limit? || result[:affected_rows] == 0
+ end
+ end
+ end
+end
diff --git a/app/services/loose_foreign_keys/cleaner_service.rb b/app/services/loose_foreign_keys/cleaner_service.rb
new file mode 100644
index 00000000000..8fe053e2edf
--- /dev/null
+++ b/app/services/loose_foreign_keys/cleaner_service.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module LooseForeignKeys
+ # rubocop: disable CodeReuse/ActiveRecord
+ class CleanerService
+ DELETE_LIMIT = 1000
+ UPDATE_LIMIT = 500
+
+ delegate :connection, to: :model
+
+ def initialize(model:, foreign_key_definition:, deleted_parent_records:, with_skip_locked: false)
+ @model = model
+ @foreign_key_definition = foreign_key_definition
+ @deleted_parent_records = deleted_parent_records
+ @with_skip_locked = with_skip_locked
+ end
+
+ def execute
+ result = connection.execute(build_query)
+
+ { affected_rows: result.cmd_tuples, table: foreign_key_definition.to_table }
+ end
+
+ def async_delete?
+ foreign_key_definition.on_delete == :async_delete
+ end
+
+ def async_nullify?
+ foreign_key_definition.on_delete == :async_nullify
+ end
+
+ private
+
+ attr_reader :model, :foreign_key_definition, :deleted_parent_records, :with_skip_locked
+
+ def build_query
+ query = if async_delete?
+ delete_query
+ elsif async_nullify?
+ update_query
+ else
+ raise "Invalid on_delete argument: #{foreign_key_definition.on_delete}"
+ end
+
+ unless query.include?(%{"#{foreign_key_definition.column}" IN (})
+ raise("FATAL: foreign key condition is missing from the generated query: #{query}")
+ end
+
+ query
+ end
+
+ def arel_table
+ @arel_table ||= model.arel_table
+ end
+
+ def primary_keys
+ @primary_keys ||= connection.primary_keys(model.table_name).map { |key| arel_table[key] }
+ end
+
+ def quoted_table_name
+ @quoted_table_name ||= Arel.sql(connection.quote_table_name(model.table_name))
+ end
+
+ def delete_query
+ query = Arel::DeleteManager.new
+ query.from(quoted_table_name)
+
+ add_in_query_with_limit(query, DELETE_LIMIT)
+ end
+
+ def update_query
+ query = Arel::UpdateManager.new
+ query.table(quoted_table_name)
+ query.set([[arel_table[foreign_key_definition.column], nil]])
+
+ add_in_query_with_limit(query, UPDATE_LIMIT)
+ end
+
+ # IN query with one or composite primary key
+ # WHERE (primary_key1, primary_key2) IN (subselect)
+ def add_in_query_with_limit(query, limit)
+ columns = Arel::Nodes::Grouping.new(primary_keys)
+ query.where(columns.in(in_query_with_limit(limit))).to_sql
+ end
+
+ # Builds the following sub-query
+ # SELECT primary_keys FROM table WHERE foreign_key IN (1, 2, 3) LIMIT N
+ def in_query_with_limit(limit)
+ in_query = Arel::SelectManager.new
+ in_query.from(quoted_table_name)
+ in_query.where(arel_table[foreign_key_definition.column].in(deleted_parent_records.map(&:primary_key_value)))
+ in_query.projections = primary_keys
+ in_query.take(limit)
+ in_query.lock(Arel.sql('FOR UPDATE SKIP LOCKED')) if with_skip_locked
+ in_query
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end
diff --git a/app/services/loose_foreign_keys/process_deleted_records_service.rb b/app/services/loose_foreign_keys/process_deleted_records_service.rb
new file mode 100644
index 00000000000..735fc8a2415
--- /dev/null
+++ b/app/services/loose_foreign_keys/process_deleted_records_service.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module LooseForeignKeys
+ class ProcessDeletedRecordsService
+ BATCH_SIZE = 1000
+
+ def initialize(connection:)
+ @connection = connection
+ end
+
+ def execute
+ modification_tracker = ModificationTracker.new
+
+ tracked_tables.cycle do |table|
+ records = load_batch_for_table(table)
+
+ if records.empty?
+ tracked_tables.delete(table)
+ next
+ end
+
+ break if modification_tracker.over_limit?
+
+ model = find_parent_model!(table)
+
+ LooseForeignKeys::BatchCleanerService
+ .new(parent_klass: model,
+ deleted_parent_records: records,
+ modification_tracker: modification_tracker,
+ models_by_table_name: models_by_table_name)
+ .execute
+
+ break if modification_tracker.over_limit?
+ end
+
+ modification_tracker.stats
+ end
+
+ private
+
+ attr_reader :connection
+
+ def load_batch_for_table(table)
+ fully_qualified_table_name = "#{current_schema}.#{table}"
+ LooseForeignKeys::DeletedRecord.load_batch_for_table(fully_qualified_table_name, BATCH_SIZE)
+ end
+
+ def find_parent_model!(table)
+ models_by_table_name.fetch(table)
+ end
+
+ def current_schema
+ @current_schema = connection.current_schema
+ end
+
+ def tracked_tables
+ @tracked_tables ||= models_by_table_name
+ .select { |table_name, model| model.respond_to?(:loose_foreign_key_definitions) }
+ .keys
+ end
+
+ def models_by_table_name
+ @models_by_table_name ||= begin
+ all_models
+ .select(&:base_class?)
+ .index_by(&:table_name)
+ end
+ end
+
+ def all_models
+ ApplicationRecord.descendants
+ end
+ end
+end
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 0cc62e661a3..cb905e01613 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -63,10 +63,14 @@ module Members
invites,
params[:access_level],
expires_at: params[:expires_at],
- current_user: current_user
+ current_user: current_user,
+ tasks_to_be_done: params[:tasks_to_be_done],
+ tasks_project_id: params[:tasks_project_id]
)
members.each { |member| process_result(member) }
+
+ create_tasks_to_be_done
end
def process_result(member)
@@ -112,6 +116,19 @@ module Members
end
end
+ def create_tasks_to_be_done
+ return unless experiment(:invite_members_for_task).enabled?
+ return if params[:tasks_to_be_done].blank? || params[:tasks_project_id].blank?
+
+ valid_members = members.select { |member| member.valid? && member.member_task.valid? }
+ return unless valid_members.present?
+
+ # We can take the first `member_task` here, since all tasks will have the same attributes needed
+ # for the `TasksToBeDone::CreateWorker`, ie. `project` and `tasks_to_be_done`.
+ member_task = valid_members[0].member_task
+ TasksToBeDone::CreateWorker.perform_async(member_task.id, current_user.id, valid_members.map(&:user_id))
+ end
+
def areas_of_focus
params[:areas_of_focus] || []
end
diff --git a/app/services/members/creator_service.rb b/app/services/members/creator_service.rb
index 7b0bebff760..f2c8a6f20a1 100644
--- a/app/services/members/creator_service.rb
+++ b/app/services/members/creator_service.rb
@@ -4,6 +4,8 @@ module Members
# This class serves as more of an app-wide way we add/create members
# All roads to add members should take this path.
class CreatorService
+ include Gitlab::Experiment::Dsl
+
class << self
def parsed_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i }
@@ -24,6 +26,7 @@ module Members
def execute
find_or_build_member
update_member
+ create_member_task
member
end
@@ -61,6 +64,21 @@ module Members
}
end
+ def create_member_task
+ return unless experiment(:invite_members_for_task).enabled?
+ return unless member.persisted?
+ return if member_task_attributes.value?(nil)
+
+ member.create_member_task(member_task_attributes)
+ end
+
+ def member_task_attributes
+ {
+ tasks_to_be_done: args[:tasks_to_be_done],
+ project_id: args[:tasks_project_id]
+ }
+ end
+
def approve_request
::Members::ApproveAccessRequestService.new(current_user,
access_level: access_level)
diff --git a/app/services/members/invite_service.rb b/app/services/members/invite_service.rb
index 257a986b8dd..85acb720f0f 100644
--- a/app/services/members/invite_service.rb
+++ b/app/services/members/invite_service.rb
@@ -39,6 +39,11 @@ module Members
errors[invite_email(member)] = member.errors.full_messages.to_sentence
end
+ override :create_tasks_to_be_done
+ def create_tasks_to_be_done
+ # Only create task issues for existing users. Tasks for new users are created when they signup.
+ end
+
def invite_email(member)
member.invite_email || member.user.email
end
diff --git a/app/services/merge_requests/outdated_discussion_diff_lines_service.rb b/app/services/merge_requests/outdated_discussion_diff_lines_service.rb
new file mode 100644
index 00000000000..a2de5a32963
--- /dev/null
+++ b/app/services/merge_requests/outdated_discussion_diff_lines_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class OutdatedDiscussionDiffLinesService
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :project, :note
+
+ OVERFLOW_LINES_COUNT = 2
+
+ def initialize(project:, note:)
+ @project = project
+ @note = note
+ end
+
+ def execute
+ end_position = position.line_range["end"]
+ diff_line_index = diff_lines.find_index do |l|
+ if end_position["new_line"]
+ l.new_line == end_position["new_line"]
+ elsif end_position["old_line"]
+ l.old_line == end_position["old_line"]
+ end
+ end
+ initial_line_index = [diff_line_index - OVERFLOW_LINES_COUNT, 0].max
+ last_line_index = [diff_line_index + OVERFLOW_LINES_COUNT, diff_lines.length].min
+
+ prev_lines = []
+
+ diff_lines[initial_line_index..last_line_index].each do |line|
+ if line.meta?
+ prev_lines.clear
+ else
+ prev_lines << line
+ end
+ end
+
+ prev_lines
+ end
+
+ private
+
+ def position
+ note.change_position
+ end
+
+ def repository
+ project.repository
+ end
+
+ def diff_file
+ position.diff_file(repository)
+ end
+
+ def diff_lines
+ strong_memoize(:diff_lines) do
+ diff_file.highlighted_diff_lines
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/retarget_chain_service.rb b/app/services/merge_requests/retarget_chain_service.rb
index dab6e198979..33aae4184ae 100644
--- a/app/services/merge_requests/retarget_chain_service.rb
+++ b/app/services/merge_requests/retarget_chain_service.rb
@@ -5,8 +5,6 @@ module MergeRequests
MAX_RETARGET_MERGE_REQUESTS = 4
def execute(merge_request)
- return unless Feature.enabled?(:retarget_merge_requests, merge_request.target_project, default_enabled: :yaml)
-
# we can only retarget MRs that are targeting the same project
return unless merge_request.for_same_project? && merge_request.merged?
diff --git a/app/services/merge_requests/toggle_attention_requested_service.rb b/app/services/merge_requests/toggle_attention_requested_service.rb
new file mode 100644
index 00000000000..66c5d6fce5d
--- /dev/null
+++ b/app/services/merge_requests/toggle_attention_requested_service.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class ToggleAttentionRequestedService < MergeRequests::BaseService
+ attr_accessor :merge_request, :user
+
+ def initialize(project:, current_user:, merge_request:, user:)
+ super(project: project, current_user: current_user)
+
+ @merge_request = merge_request
+ @user = user
+ end
+
+ def execute
+ return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
+
+ if reviewer || assignee
+ update_state(reviewer)
+ update_state(assignee)
+
+ if reviewer&.attention_requested? || assignee&.attention_requested?
+ notity_user
+ end
+
+ success
+ else
+ error("User is not a reviewer or assignee of the merge request")
+ end
+ end
+
+ private
+
+ def notity_user
+ todo_service.create_attention_requested_todo(merge_request, current_user, user)
+ end
+
+ def assignee
+ merge_request.find_assignee(user)
+ end
+
+ def reviewer
+ merge_request.find_reviewer(user)
+ end
+
+ def update_state(reviewer_or_assignee)
+ reviewer_or_assignee&.update(state: reviewer_or_assignee&.attention_requested? ? :reviewed : :attention_requested)
+ end
+ end
+end
diff --git a/app/services/namespaces/in_product_marketing_email_records.rb b/app/services/namespaces/in_product_marketing_email_records.rb
new file mode 100644
index 00000000000..1237a05ea13
--- /dev/null
+++ b/app/services/namespaces/in_product_marketing_email_records.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Namespaces
+ class InProductMarketingEmailRecords
+ attr_reader :records
+
+ def initialize
+ @records = []
+ end
+
+ def save!
+ Users::InProductMarketingEmail.bulk_insert!(@records)
+ @records = []
+ end
+
+ def add(user, track, series)
+ @records << Users::InProductMarketingEmail.new(
+ user: user,
+ track: track,
+ series: series,
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now
+ )
+ end
+ end
+end
diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb
index 0401653cf3c..90900698e1a 100644
--- a/app/services/namespaces/in_product_marketing_emails_service.rb
+++ b/app/services/namespaces/in_product_marketing_emails_service.rb
@@ -56,7 +56,7 @@ module Namespaces
def initialize(track, interval)
@track = track
@interval = interval
- @in_product_marketing_email_records = []
+ @sent_email_records = InProductMarketingEmailRecords.new
end
def execute
@@ -71,17 +71,21 @@ module Namespaces
private
- attr_reader :track, :interval, :in_product_marketing_email_records
+ attr_reader :track, :interval, :sent_email_records
+
+ def send_email(user, group)
+ NotificationService.new.in_product_marketing(user.id, group.id, track, series)
+ end
def send_email_for_group(group)
users_for_group(group).each do |user|
if can_perform_action?(user, group)
send_email(user, group)
- track_sent_email(user, track, series)
+ sent_email_records.add(user, track, series)
end
end
- save_tracked_emails!
+ sent_email_records.save!
end
def groups_for_track
@@ -126,10 +130,6 @@ module Namespaces
end
end
- def send_email(user, group)
- NotificationService.new.in_product_marketing(user.id, group.id, track, series)
- end
-
def completed_actions
TRACKS[track][:completed_actions]
end
@@ -146,21 +146,6 @@ module Namespaces
def series
TRACKS[track][:interval_days].index(interval)
end
-
- def save_tracked_emails!
- Users::InProductMarketingEmail.bulk_insert!(in_product_marketing_email_records)
- @in_product_marketing_email_records = []
- end
-
- def track_sent_email(user, track, series)
- in_product_marketing_email_records << Users::InProductMarketingEmail.new(
- user: user,
- track: track,
- series: series,
- created_at: Time.zone.now,
- updated_at: Time.zone.now
- )
- end
end
end
diff --git a/app/services/namespaces/invite_team_email_service.rb b/app/services/namespaces/invite_team_email_service.rb
new file mode 100644
index 00000000000..45975d1953a
--- /dev/null
+++ b/app/services/namespaces/invite_team_email_service.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Namespaces
+ class InviteTeamEmailService
+ include Gitlab::Experiment::Dsl
+
+ TRACK = :invite_team
+ DELIVERY_DELAY_IN_MINUTES = 20.minutes
+
+ def self.send_email(user, group)
+ new(user, group).execute
+ end
+
+ def initialize(user, group)
+ @group = group
+ @user = user
+ @sent_email_records = InProductMarketingEmailRecords.new
+ end
+
+ def execute
+ return unless user.email_opted_in?
+ return unless group.root?
+ return unless group.setup_for_company
+
+ # Exclude group if users other than the creator have already been
+ # added/invited
+ return unless group.member_count == 1
+
+ return if email_for_track_sent_to_user?
+
+ experiment(:invite_team_email, group: group) do |e|
+ e.candidate do
+ send_email(user, group)
+ sent_email_records.add(user, track, series)
+ sent_email_records.save!
+ end
+
+ e.record!
+ end
+ end
+
+ private
+
+ attr_reader :user, :group, :sent_email_records
+
+ def send_email(user, group)
+ NotificationService.new.in_product_marketing(user.id, group.id, track, series)
+ end
+
+ def track
+ TRACK
+ end
+
+ def series
+ 0
+ end
+
+ def email_for_track_sent_to_user?
+ Users::InProductMarketingEmail.for_user_with_track_and_series(user, track, series).present?
+ end
+ end
+end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 194c3d7bf7b..9a0db3bb9aa 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -43,7 +43,7 @@ module Notes
private
def execute_quick_actions(note)
- return yield(false) unless quick_actions_service.supported?(note)
+ return yield(false) unless quick_actions_supported?(note)
content, update_params, message = quick_actions_service.execute(note, quick_action_options)
only_commands = content.empty?
@@ -54,6 +54,10 @@ module Notes
do_commands(note, update_params, message, only_commands)
end
+ def quick_actions_supported?(note)
+ quick_actions_service.supported?(note)
+ end
+
def quick_actions_service
@quick_actions_service ||= QuickActionsService.new(project, current_user)
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index afc9015e758..6ad3a74b85d 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -598,8 +598,8 @@ class NotificationService
user.notification_email_for(pipeline.project.group)
end
- if recipients.any?
- mailer.public_send(email_template, pipeline, recipients).deliver_later
+ recipients.each do |recipient|
+ mailer.public_send(email_template, pipeline, recipient).deliver_later
end
end
diff --git a/app/services/packages/create_dependency_service.rb b/app/services/packages/create_dependency_service.rb
index 2c80ec66dbc..10a86e44cb0 100644
--- a/app/services/packages/create_dependency_service.rb
+++ b/app/services/packages/create_dependency_service.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
module Packages
+ # rubocop: disable Gitlab/BulkInsert
class CreateDependencyService < BaseService
attr_reader :package, :dependencies
@@ -51,7 +52,7 @@ module Packages
}
end
- ids = database.bulk_insert(Packages::Dependency.table_name, rows, return_ids: true, on_conflict: :do_nothing)
+ ids = ApplicationRecord.legacy_bulk_insert(Packages::Dependency.table_name, rows, return_ids: true, on_conflict: :do_nothing)
return ids if ids.size == names_and_version_patterns.size
Packages::Dependency.uncached do
@@ -72,11 +73,8 @@ module Packages
}
end
- database.bulk_insert(Packages::DependencyLink.table_name, rows)
- end
-
- def database
- ::Gitlab::Database.main
+ ApplicationRecord.legacy_bulk_insert(Packages::DependencyLink.table_name, rows)
end
end
+ # rubocop: enable Gitlab/BulkInsert
end
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index 1d5d9c38432..ae9c92a3d3a 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -4,6 +4,8 @@ module Packages
class CreatePackageService < ::Packages::CreatePackageService
include Gitlab::Utils::StrongMemoize
+ PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename].freeze
+
def execute
return error('Version is empty.', 400) if version.blank?
return error('Package already exists.', 403) if current_package_exists?
@@ -21,6 +23,10 @@ module Packages
::Packages::CreateDependencyService.new(package, package_dependencies).execute
::Packages::Npm::CreateTagService.new(package, dist_tag).execute
+ if Feature.enabled?(:packages_npm_abbreviated_metadata, project, default_enabled: :yaml)
+ package.create_npm_metadatum!(package_json: package_json)
+ end
+
package
end
@@ -46,6 +52,10 @@ module Packages
params[:versions][version]
end
+ def package_json
+ version_data.except(*PACKAGE_JSON_NOT_ALLOWED_FIELDS)
+ end
+
def dist_tag
params['dist-tags'].each_key.first
end
diff --git a/app/services/packages/nuget/create_dependency_service.rb b/app/services/packages/nuget/create_dependency_service.rb
index 3fc42056d43..85f295ac7b7 100644
--- a/app/services/packages/nuget/create_dependency_service.rb
+++ b/app/services/packages/nuget/create_dependency_service.rb
@@ -41,7 +41,7 @@ module Packages
}
end
- ::Gitlab::Database.main.bulk_insert(::Packages::Nuget::DependencyLinkMetadatum.table_name, rows.compact) # rubocop:disable Gitlab/BulkInsert
+ ::ApplicationRecord.legacy_bulk_insert(::Packages::Nuget::DependencyLinkMetadatum.table_name, rows.compact) # rubocop:disable Gitlab/BulkInsert
end
def raw_dependency_for(dependency)
diff --git a/app/services/packages/rubygems/create_dependencies_service.rb b/app/services/packages/rubygems/create_dependencies_service.rb
index dea429148cf..0b2ae56bf45 100644
--- a/app/services/packages/rubygems/create_dependencies_service.rb
+++ b/app/services/packages/rubygems/create_dependencies_service.rb
@@ -3,8 +3,6 @@
module Packages
module Rubygems
class CreateDependenciesService
- include BulkInsertSafe
-
def initialize(package, gemspec)
@package = package
@gemspec = gemspec
diff --git a/app/services/packages/update_tags_service.rb b/app/services/packages/update_tags_service.rb
index 2bdf75a6617..f29c54dacb9 100644
--- a/app/services/packages/update_tags_service.rb
+++ b/app/services/packages/update_tags_service.rb
@@ -15,7 +15,7 @@ module Packages
tags_to_create = @tags - existing_tags
@package.tags.with_name(tags_to_destroy).delete_all if tags_to_destroy.any?
- ::Gitlab::Database.main.bulk_insert(Packages::Tag.table_name, rows(tags_to_create)) if tags_to_create.any? # rubocop:disable Gitlab/BulkInsert
+ ::ApplicationRecord.legacy_bulk_insert(Packages::Tag.table_name, rows(tags_to_create)) if tags_to_create.any? # rubocop:disable Gitlab/BulkInsert
end
private
diff --git a/app/services/projects/alerting/notify_service.rb b/app/services/projects/alerting/notify_service.rb
index a5ee7173bdf..e5d40b60747 100644
--- a/app/services/projects/alerting/notify_service.rb
+++ b/app/services/projects/alerting/notify_service.rb
@@ -5,6 +5,7 @@ module Projects
class NotifyService
extend ::Gitlab::Utils::Override
include ::AlertManagement::AlertProcessing
+ include ::AlertManagement::Responses
def initialize(project, payload)
@project = project
@@ -23,7 +24,7 @@ module Projects
complete_post_processing_tasks
- ServiceResponse.success
+ success(alert)
end
private
@@ -46,18 +47,6 @@ module Projects
def valid_token?(token)
token == integration.token
end
-
- def bad_request
- ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
- end
-
- def unauthorized
- ServiceResponse.error(message: 'Unauthorized', http_status: :unauthorized)
- end
-
- def forbidden
- ServiceResponse.error(message: 'Forbidden', http_status: :forbidden)
- end
end
end
end
diff --git a/app/services/projects/all_issues_count_service.rb b/app/services/projects/all_issues_count_service.rb
new file mode 100644
index 00000000000..15352b14d3e
--- /dev/null
+++ b/app/services/projects/all_issues_count_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Projects
+ # Service class for counting and caching the number of all issues of a
+ # project.
+ class AllIssuesCountService < Projects::CountService
+ def relation_for_count
+ @project.issues
+ end
+
+ def cache_key_name
+ 'all_issues_count'
+ end
+ end
+end
diff --git a/app/services/projects/all_merge_requests_count_service.rb b/app/services/projects/all_merge_requests_count_service.rb
new file mode 100644
index 00000000000..db0bab3f799
--- /dev/null
+++ b/app/services/projects/all_merge_requests_count_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Projects
+ # Service class for counting and caching the number of all merge requests of
+ # a project.
+ class AllMergeRequestsCountService < Projects::CountService
+ def relation_for_count
+ @project.merge_requests
+ end
+
+ def cache_key_name
+ 'all_merge_requests_count'
+ end
+ end
+end
diff --git a/app/services/projects/container_repository/cache_tags_created_at_service.rb b/app/services/projects/container_repository/cache_tags_created_at_service.rb
deleted file mode 100644
index 3a5346d7a23..00000000000
--- a/app/services/projects/container_repository/cache_tags_created_at_service.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module ContainerRepository
- class CacheTagsCreatedAtService
- def initialize(container_repository)
- @container_repository = container_repository
- @cached_tag_names = Set.new
- end
-
- def populate(tags)
- return if tags.empty?
-
- # This will load all tags in one Redis roundtrip
- # the maximum number of tags is configurable and is set to 200 by default.
- # https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/packages/container_registry/index.md#set-cleanup-limits-to-conserve-resources
- keys = tags.map(&method(:cache_key))
- cached_tags_count = 0
-
- ::Gitlab::Redis::Cache.with do |redis|
- tags.zip(redis.mget(keys)).each do |tag, created_at|
- next unless created_at
-
- tag.created_at = DateTime.rfc3339(created_at)
- @cached_tag_names << tag.name
- cached_tags_count += 1
- end
- end
-
- cached_tags_count
- end
-
- def insert(tags, max_ttl_in_seconds)
- return unless max_ttl_in_seconds
- return if tags.empty?
-
- # tags with nil created_at are not cacheable
- # tags already cached don't need to be cached again
- cacheable_tags = tags.select do |tag|
- tag.created_at.present? && !tag.name.in?(@cached_tag_names)
- end
-
- return if cacheable_tags.empty?
-
- now = Time.zone.now
-
- ::Gitlab::Redis::Cache.with do |redis|
- # we use a pipeline instead of a MSET because each tag has
- # a specific ttl
- redis.pipelined do
- cacheable_tags.each do |tag|
- created_at = tag.created_at
- # ttl is the max_ttl_in_seconds reduced by the number
- # of seconds that the tag has already existed
- ttl = max_ttl_in_seconds - (now - created_at).seconds
- ttl = ttl.to_i
- redis.set(cache_key(tag), created_at.rfc3339, ex: ttl) if ttl > 0
- end
- end
- end
- end
-
- private
-
- def cache_key(tag)
- "container_repository:{#{@container_repository.id}}:tag:#{tag.name}:created_at"
- end
- end
- end
-end
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb
index 3a60de0f1ee..1a788abac12 100644
--- a/app/services/projects/container_repository/cleanup_tags_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_service.rb
@@ -140,14 +140,13 @@ module Projects
def cache
strong_memoize(:cache) do
- ::Projects::ContainerRepository::CacheTagsCreatedAtService.new(@container_repository)
+ ::Gitlab::ContainerRepository::Tags::Cache.new(@container_repository)
end
end
def caching_enabled?
container_expiration_policy &&
- older_than.present? &&
- Feature.enabled?(:container_registry_expiration_policies_caching, @project)
+ older_than.present?
end
def throttling_enabled?
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 1536f0a22b8..1d187b140ef 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -45,7 +45,7 @@ module Projects
if namespace_id
# Find matching namespace and check if it allowed
# for current user if namespace_id passed.
- unless current_user.can?(:create_projects, project_namespace)
+ unless current_user.can?(:create_projects, parent_namespace)
@project.namespace_id = nil
deny_namespace
return @project
@@ -136,7 +136,7 @@ module Projects
access_level: group_access_level)
end
- AuthorizedProjectUpdate::ProjectCreateWorker.perform_async(@project.id)
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(@project.id)
# AuthorizedProjectsWorker uses an exclusive lease per user but
# specialized workers might have synchronization issues. Until we
# compare the inconsistency rates of both approaches, we still run
@@ -227,14 +227,14 @@ module Projects
def extra_attributes_for_measurement
{
current_user: current_user&.name,
- project_full_path: "#{project_namespace&.full_path}/#{@params[:path]}"
+ project_full_path: "#{parent_namespace&.full_path}/#{@params[:path]}"
}
end
private
- def project_namespace
- @project_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace
+ def parent_namespace
+ @parent_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace
end
def create_from_template?
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 27f813f4661..b7ed9202b01 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -152,9 +152,12 @@ module Projects
deleted_count = project.commit_statuses.delete_all
- if deleted_count > 0
- Gitlab::AppLogger.info "Projects::DestroyService - Project #{project.id} - #{deleted_count} leftover commit statuses"
- end
+ Gitlab::AppLogger.info(
+ class: 'Projects::DestroyService',
+ project_id: project.id,
+ message: 'leftover commit statuses',
+ orphaned_commit_status_count: deleted_count
+ )
end
# The project can have multiple webhooks with hundreds of thousands of web_hook_logs.
diff --git a/app/services/projects/detect_repository_languages_service.rb b/app/services/projects/detect_repository_languages_service.rb
index 0356a6b0ccd..9db0b71d106 100644
--- a/app/services/projects/detect_repository_languages_service.rb
+++ b/app/services/projects/detect_repository_languages_service.rb
@@ -21,7 +21,7 @@ module Projects
.update_all(share: update[:share])
end
- Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert( # rubocop:disable Gitlab/BulkInsert
RepositoryLanguage.table_name,
detection.insertions(matching_programming_languages)
)
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 64c0f1ff4ac..b1a2182fbdc 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -36,6 +36,7 @@ module Projects
private
attr_accessor :shared
+ attr_reader :logger
def execute_after_export_action(after_export_strategy)
return unless after_export_strategy
@@ -74,7 +75,11 @@ module Projects
end
def project_tree_saver
- tree_saver_class.new(project: project, current_user: current_user, shared: shared, params: params)
+ tree_saver_class.new(project: project,
+ current_user: current_user,
+ shared: shared,
+ params: params,
+ logger: logger)
end
def tree_saver_class
@@ -116,7 +121,7 @@ module Projects
end
def notify_success
- @logger.info(
+ logger.info(
message: 'Project successfully exported',
project_name: project.name,
project_id: project.id
@@ -124,7 +129,7 @@ module Projects
end
def notify_error
- @logger.error(
+ logger.error(
message: 'Project export error',
export_errors: shared.errors.join(', '),
project_name: project.name,
diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb
index 7c00b9e6105..cf3cc5cd8e0 100644
--- a/app/services/projects/lfs_pointers/lfs_link_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_link_service.rb
@@ -38,7 +38,7 @@ module Projects
rows = existent_lfs_objects
.not_linked_to_project(project)
.map { |existing_lfs_object| { project_id: project.id, lfs_object_id: existing_lfs_object.id } }
- Gitlab::Database.main.bulk_insert(:lfs_objects_projects, rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(:lfs_objects_projects, rows) # rubocop:disable Gitlab/BulkInsert
iterations += 1
linked_existing_objects += existent_lfs_objects.map(&:oid)
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index 1616a8a4062..152590fffff 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -36,17 +36,9 @@ module Projects
private
def project_members_through_invited_groups
- groups_with_ancestors = if ::Feature.enabled?(:linear_participants_service_ancestor_scopes, current_user, default_enabled: :yaml)
- visible_groups.self_and_ancestors
- else
- Gitlab::ObjectHierarchy
- .new(visible_groups)
- .base_and_ancestors
- end
-
GroupMember
.active_without_invites_and_requests
- .with_source_id(groups_with_ancestors.pluck_primary_key)
+ .with_source_id(visible_groups.self_and_ancestors.pluck_primary_key)
end
def visible_groups
diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb
index c1bf2e68436..56f65718d24 100644
--- a/app/services/projects/prometheus/alerts/notify_service.rb
+++ b/app/services/projects/prometheus/alerts/notify_service.rb
@@ -6,6 +6,7 @@ module Projects
class NotifyService
include Gitlab::Utils::StrongMemoize
include ::IncidentManagement::Settings
+ include ::AlertManagement::Responses
# This set of keys identifies a payload as a valid Prometheus
# payload and thus processable by this service. See also
@@ -27,9 +28,9 @@ module Projects
return unprocessable_entity unless self.class.processable?(payload)
return unauthorized unless valid_alert_manager_token?(token, integration)
- process_prometheus_alerts
+ alert_responses = process_prometheus_alerts
- ServiceResponse.success
+ alert_response(alert_responses)
end
def self.processable?(payload)
@@ -128,23 +129,17 @@ module Projects
end
def process_prometheus_alerts
- alerts.each do |alert|
+ alerts.map do |alert|
AlertManagement::ProcessPrometheusAlertService
.new(project, alert.to_h)
.execute
end
end
- def bad_request
- ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
- end
-
- def unauthorized
- ServiceResponse.error(message: 'Unauthorized', http_status: :unauthorized)
- end
+ def alert_response(alert_responses)
+ alerts = alert_responses.map { |resp| resp.payload[:alert] }.compact
- def unprocessable_entity
- ServiceResponse.error(message: 'Unprocessable Entity', http_status: :unprocessable_entity)
+ success(alerts)
end
end
end
diff --git a/app/services/resource_events/base_synthetic_notes_builder_service.rb b/app/services/resource_events/base_synthetic_notes_builder_service.rb
index 5939b9d2f9c..192d40129a3 100644
--- a/app/services/resource_events/base_synthetic_notes_builder_service.rb
+++ b/app/services/resource_events/base_synthetic_notes_builder_service.rb
@@ -24,10 +24,18 @@ module ResourceEvents
private
def apply_common_filters(events)
+ events = apply_pagination(events)
events = apply_last_fetched_at(events)
apply_fetch_until(events)
end
+ def apply_pagination(events)
+ return events if params[:paginated_notes].nil?
+ return events.none if params[:paginated_notes][table_name].blank?
+
+ events.id_in(params[:paginated_notes][table_name].map(&:id))
+ end
+
def apply_last_fetched_at(events)
return events unless params[:last_fetched_at].present?
@@ -47,5 +55,9 @@ module ResourceEvents
resource.project || resource.group
end
end
+
+ def table_name
+ raise NotImplementedError
+ end
end
end
diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb
index bc2d3a946cc..03ac839c509 100644
--- a/app/services/resource_events/change_labels_service.rb
+++ b/app/services/resource_events/change_labels_service.rb
@@ -23,7 +23,7 @@ module ResourceEvents
label_hash.merge(label_id: label.id, action: ResourceLabelEvent.actions['remove'])
end
- Gitlab::Database.main.bulk_insert(ResourceLabelEvent.table_name, labels) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(ResourceLabelEvent.table_name, labels) # rubocop:disable Gitlab/BulkInsert
resource.expire_note_etag_cache
Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_label_changed_action(author: user) if resource.is_a?(Issue)
diff --git a/app/services/resource_events/synthetic_label_notes_builder_service.rb b/app/services/resource_events/synthetic_label_notes_builder_service.rb
index 5915ea938cf..0e5d945d13c 100644
--- a/app/services/resource_events/synthetic_label_notes_builder_service.rb
+++ b/app/services/resource_events/synthetic_label_notes_builder_service.rb
@@ -23,5 +23,9 @@ module ResourceEvents
events.group_by { |event| event.discussion_id }
end
+
+ def table_name
+ 'resource_label_events'
+ end
end
end
diff --git a/app/services/resource_events/synthetic_milestone_notes_builder_service.rb b/app/services/resource_events/synthetic_milestone_notes_builder_service.rb
index 10acf94e22b..0e2b171e192 100644
--- a/app/services/resource_events/synthetic_milestone_notes_builder_service.rb
+++ b/app/services/resource_events/synthetic_milestone_notes_builder_service.rb
@@ -21,5 +21,9 @@ module ResourceEvents
events = resource.resource_milestone_events.includes(user: :status) # rubocop: disable CodeReuse/ActiveRecord
apply_common_filters(events)
end
+
+ def table_name
+ 'resource_milestone_events'
+ end
end
end
diff --git a/app/services/resource_events/synthetic_state_notes_builder_service.rb b/app/services/resource_events/synthetic_state_notes_builder_service.rb
index 71d40200365..e17882b00de 100644
--- a/app/services/resource_events/synthetic_state_notes_builder_service.rb
+++ b/app/services/resource_events/synthetic_state_notes_builder_service.rb
@@ -16,5 +16,9 @@ module ResourceEvents
events = resource.resource_state_events.includes(user: :status) # rubocop: disable CodeReuse/ActiveRecord
apply_common_filters(events)
end
+
+ def table_name
+ 'resource_state_events'
+ end
end
end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index cce7821a226..4ba1b3ade86 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -75,6 +75,10 @@ class SearchService
search_results.highlight_map(scope)
end
+ def search_aggregations
+ search_results.aggregations(scope)
+ end
+
private
def page
diff --git a/app/services/security/ci_configuration/sast_iac_create_service.rb b/app/services/security/ci_configuration/sast_iac_create_service.rb
new file mode 100644
index 00000000000..80e9cf963da
--- /dev/null
+++ b/app/services/security/ci_configuration/sast_iac_create_service.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Security
+ module CiConfiguration
+ class SastIacCreateService < ::Security::CiConfiguration::BaseCreateService
+ private
+
+ def action
+ Security::CiConfiguration::SastIacBuildAction.new(project.auto_devops_enabled?, existing_gitlab_ci_content).generate
+ end
+
+ def next_branch
+ 'set-sast-iac-config'
+ end
+
+ def message
+ _('Configure SAST IaC in `.gitlab-ci.yml`, creating this file if it does not already exist')
+ end
+
+ def description
+ _('Configure SAST IaC in `.gitlab-ci.yml` using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings) to customize SAST IaC settings.')
+ end
+ end
+ end
+end
diff --git a/app/services/snippets/update_service.rb b/app/services/snippets/update_service.rb
index d83b21271c0..76d5063c337 100644
--- a/app/services/snippets/update_service.rb
+++ b/app/services/snippets/update_service.rb
@@ -148,3 +148,5 @@ module Snippets
end
end
end
+
+Snippets::UpdateService.prepend_mod_with('Snippets::UpdateService')
diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb
index 8d995631db6..c8bdcf4310b 100644
--- a/app/services/spam/spam_verdict_service.rb
+++ b/app/services/spam/spam_verdict_service.rb
@@ -73,18 +73,12 @@ module Spam
begin
result, attribs, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context)
- return [nil, attribs] unless result
-
# @TODO log if error is not nil https://gitlab.com/gitlab-org/gitlab/-/issues/329545
- return [result, attribs] if result == NOOP || attribs["monitorMode"] == "true"
+ return [nil, attribs] unless result
+
+ [result, attribs]
- # Duplicate logic with Akismet logic in #akismet_verdict
- if Gitlab::Recaptcha.enabled? && result != ALLOW
- [CONDITIONAL_ALLOW, attribs]
- else
- [result, attribs]
- end
rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e)
diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb
index eb98ed57d55..239cd86e0ec 100644
--- a/app/services/suggestions/create_service.rb
+++ b/app/services/suggestions/create_service.rb
@@ -25,7 +25,7 @@ module Suggestions
end
rows.in_groups_of(100, false) do |rows|
- Gitlab::Database.main.bulk_insert('suggestions', rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert('suggestions', rows) # rubocop:disable Gitlab/BulkInsert
end
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter.track_add_suggestion_action(note: @note)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index e5080718b69..dc5cf0fe554 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -327,6 +327,10 @@ module SystemNoteService
::SystemNotes::IncidentService.new(noteable: incident, project: incident.project, author: author).change_incident_severity
end
+ def resolve_incident_status(incident, author)
+ ::SystemNotes::IncidentService.new(noteable: incident, project: incident.project, author: author).resolve_incident_status
+ end
+
def log_resolving_alert(alert, monitoring_tool)
::SystemNotes::AlertManagementService.new(noteable: alert, project: alert.project).log_resolving_alert(monitoring_tool)
end
diff --git a/app/services/system_notes/incident_service.rb b/app/services/system_notes/incident_service.rb
index 4628662f0e9..785291e0637 100644
--- a/app/services/system_notes/incident_service.rb
+++ b/app/services/system_notes/incident_service.rb
@@ -25,5 +25,11 @@ module SystemNotes
)
end
end
+
+ def resolve_incident_status
+ body = 'changed the status to **Resolved** by closing the incident'
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'status'))
+ end
end
end
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 62aead352aa..94629ae7609 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -176,7 +176,13 @@ module SystemNotes
body = cross_reference_note_content(gfm_reference)
if noteable.is_a?(ExternalIssue)
- noteable.project.external_issue_tracker.create_cross_reference_note(noteable, mentioner, author)
+ Integrations::CreateExternalCrossReferenceWorker.perform_async(
+ noteable.project_id,
+ noteable.id,
+ mentioner.class.name,
+ mentioner.id,
+ author.id
+ )
else
track_cross_reference_action
create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference'))
diff --git a/app/services/tasks_to_be_done/base_service.rb b/app/services/tasks_to_be_done/base_service.rb
new file mode 100644
index 00000000000..a5648ad10c4
--- /dev/null
+++ b/app/services/tasks_to_be_done/base_service.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module TasksToBeDone
+ class BaseService < ::IssuableBaseService
+ LABEL_PREFIX = 'tasks to be done'
+
+ def initialize(project:, current_user:, assignee_ids: [])
+ params = {
+ assignee_ids: assignee_ids,
+ title: title,
+ description: description,
+ add_labels: label_name
+ }
+ super(project: project, current_user: current_user, params: params)
+ end
+
+ def execute
+ if (issue = existing_task_issue)
+ update_service = Issues::UpdateService.new(project: project, current_user: current_user, params: { add_assignee_ids: params[:assignee_ids] })
+ update_service.execute(issue)
+ else
+ build_service = Issues::BuildService.new(project: project, current_user: current_user, params: params)
+ create(build_service.execute)
+ end
+ end
+
+ private
+
+ def existing_task_issue
+ IssuesFinder.new(
+ current_user,
+ project_id: project.id,
+ state: 'opened',
+ non_archived: true,
+ label_name: label_name
+ ).execute.last
+ end
+
+ def title
+ raise NotImplementedError
+ end
+
+ def description
+ raise NotImplementedError
+ end
+
+ def label_suffix
+ raise NotImplementedError
+ end
+
+ def label_name
+ "#{LABEL_PREFIX}:#{label_suffix}"
+ end
+ end
+end
diff --git a/app/services/tasks_to_be_done/create_ci_task_service.rb b/app/services/tasks_to_be_done/create_ci_task_service.rb
new file mode 100644
index 00000000000..025ca2feb8e
--- /dev/null
+++ b/app/services/tasks_to_be_done/create_ci_task_service.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module TasksToBeDone
+ class CreateCiTaskService < BaseService
+ protected
+
+ def title
+ 'Set up CI/CD'
+ end
+
+ def description
+ <<~DESCRIPTION
+ GitLab CI/CD is a tool built into GitLab for software development through the [continuous methodologies](https://docs.gitlab.com/ee/ci/introduction/index.html#introduction-to-cicd-methodologies):
+
+ * Continuous Integration (CI)
+ * Continuous Delivery (CD)
+ * Continuous Deployment (CD)
+
+ Continuous Integration works by pushing small changes to your application’s codebase hosted in a Git repository, and, to every push, run a pipeline of scripts to build, test, and validate the code changes before merging them into the main branch.
+
+ Continuous Delivery and Deployment consist of a step further CI, deploying your application to production at every push to the default branch of the repository.
+
+ These methodologies allow you to catch bugs and errors early in the development cycle, ensuring that all the code deployed to production complies with the code standards you established for your app.
+
+ * :book: [Read the documentation](https://docs.gitlab.com/ee/ci/introduction/index.html)
+ * :clapper: [Watch a Demo](https://www.youtube.com/watch?v=1iXFbchozdY)
+
+ ## Next steps
+
+ * [ ] To start we recommend reviewing the following documentation:
+ * [ ] [How GitLab CI/CD works.](https://docs.gitlab.com/ee/ci/introduction/index.html#how-gitlab-cicd-works)
+ * [ ] [Fundamental pipeline architectures.](https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html)
+ * [ ] [GitLab CI/CD basic workflow.](https://docs.gitlab.com/ee/ci/introduction/index.html#basic-cicd-workflow)
+ * [ ] [Step-by-step guide for writing .gitlab-ci.yml for the first time.](https://docs.gitlab.com/ee/user/project/pages/getting_started_part_four.html)
+ * [ ] When you're ready select **Projects** (in the top navigation bar) > **Your projects** > select the Project you've already created.
+ * [ ] Select **CI / CD** in the left navigation to start setting up CI / CD in your project.
+ DESCRIPTION
+ end
+
+ def label_suffix
+ 'ci'
+ end
+ end
+end
diff --git a/app/services/tasks_to_be_done/create_code_task_service.rb b/app/services/tasks_to_be_done/create_code_task_service.rb
new file mode 100644
index 00000000000..dc3b9366a66
--- /dev/null
+++ b/app/services/tasks_to_be_done/create_code_task_service.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module TasksToBeDone
+ class CreateCodeTaskService < BaseService
+ protected
+
+ def title
+ 'Create or import your code into your Project (Repository)'
+ end
+
+ def description
+ <<~DESCRIPTION
+ You've already created your Group and Project within GitLab; we'll quickly review this hierarchy below. Once you're within your project you can easily create or import repositories.
+
+ **With GitLab Groups, you can:**
+
+ * Create one or multiple Projects for hosting your codebase (repositories).
+ * Assemble related projects together.
+ * Grant members access to several projects at once.
+
+ Groups can also be nested in subgroups.
+
+ Read more about groups in our [documentation](https://docs.gitlab.com/ee/user/group/).
+
+ **Within GitLab Projects, you can**
+
+ * Use it as an issue tracker.
+ * Collaborate on code.
+ * Continuously build, test, and deploy your app with built-in GitLab CI/CD.
+
+ You can also import an existing repository by providing the Git URL.
+
+ * :book: [Read the documentation](https://docs.gitlab.com/ee/user/project/index.html).
+
+ ## Next steps
+
+ Create or import your first repository into the project you created:
+
+ * [ ] Click **Projects** in the top navigation bar, then click **Your projects**.
+ * [ ] Select the Project that you created, then select **Repository**.
+ * [ ] Once on the Repository page you can select the **+** icon to add or import files.
+ * [ ] You can review our full documentation on creating [repositories](https://docs.gitlab.com/ee/user/project/repository/) in GitLab.
+
+ :tada: All done, you can close this issue!
+ DESCRIPTION
+ end
+
+ def label_suffix
+ 'code'
+ end
+ end
+end
diff --git a/app/services/tasks_to_be_done/create_issues_task_service.rb b/app/services/tasks_to_be_done/create_issues_task_service.rb
new file mode 100644
index 00000000000..a2de6852868
--- /dev/null
+++ b/app/services/tasks_to_be_done/create_issues_task_service.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module TasksToBeDone
+ class CreateIssuesTaskService < BaseService
+ protected
+
+ def title
+ 'Create/import issues (tickets) to collaborate on ideas and plan work'
+ end
+
+ def description
+ <<~DESCRIPTION
+ Issues allow you and your team to discuss proposals before, and during, their implementation. They can be used for a variety of other purposes, customized to your needs and workflow.
+
+ Issues are always associated with a specific project. If you have multiple projects in a group, you can view all the issues at the group level. [You can review our full Issue documentation here.](https://docs.gitlab.com/ee/user/project/issues/)
+
+ If you have existing issues or equivalent tickets you can import them as long as they are formatted as a CSV file, [the import process is covered here](https://docs.gitlab.com/ee/user/project/issues/csv_import.html).
+
+ **Common use cases include:**
+
+ * Discussing the implementation of a new idea
+ * Tracking tasks and work status
+ * Accepting feature proposals, questions, support requests, or bug reports
+ * Elaborating on new code implementations
+
+ ## Next steps
+
+ * [ ] Select **Projects** in the top navigation > **Your Projects** > select the Project you've already created.
+ * [ ] Once you've selected that project, you can select **Issues** in the left navigation, then click **New issue**.
+ * [ ] Fill in the title and description in the **New issue** page.
+ * [ ] Click on **Create issue**.
+
+ Pro tip: When you're in a group or project you can always utilize the **+** icon in the top navigation (located to the left of the search bar) to quickly create new issues.
+
+ That's it! You can close this issue.
+ DESCRIPTION
+ end
+
+ def label_suffix
+ 'issues'
+ end
+ end
+end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 71bb813f384..091f441831a 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -217,6 +217,11 @@ class TodoService
create_todos(reviewers, attributes)
end
+ def create_attention_requested_todo(target, author, users)
+ attributes = attributes_for_todo(target.project, target, author, Todo::ATTENTION_REQUESTED)
+ create_todos(users, attributes)
+ end
+
private
def create_todos(users, attributes)
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 4ec875098fa..1634cc017ae 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -65,7 +65,10 @@ module Users
user.destroy_dependent_associations_in_batches(exclude: [:snippets])
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
- user_data = user.destroy
+ user_data = nil
+ ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/340260') do
+ user_data = user.destroy
+ end
namespace.destroy
user_data
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 1850fa9747d..2d9766c3c56 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -67,10 +67,8 @@ module Users
def update_authorizations(remove = [], add = [])
log_refresh_details(remove, add)
- User.transaction do
- user.remove_project_authorizations(remove) unless remove.empty?
- ProjectAuthorization.insert_authorizations(add) unless add.empty?
- end
+ user.remove_project_authorizations(remove) unless remove.empty?
+ ProjectAuthorization.insert_authorizations(add) unless add.empty?
# Since we batch insert authorization rows, Rails' associations may get
# out of sync. As such we force a reload of the User object.
diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb
index 86b5b923418..61cf598f178 100644
--- a/app/services/users/upsert_credit_card_validation_service.rb
+++ b/app/services/users/upsert_credit_card_validation_service.rb
@@ -12,6 +12,7 @@ module Users
credit_card_validated_at: params.fetch(:credit_card_validated_at),
expiration_date: get_expiration_date(params),
last_digits: Integer(params.fetch(:credit_card_mask_number), 10),
+ network: params.fetch(:credit_card_type),
holder_name: params.fetch(:credit_card_holder_name)
}
diff --git a/app/uploaders/bulk_imports/export_uploader.rb b/app/uploaders/bulk_imports/export_uploader.rb
index 356e5ce028e..cd6e599054b 100644
--- a/app/uploaders/bulk_imports/export_uploader.rb
+++ b/app/uploaders/bulk_imports/export_uploader.rb
@@ -2,6 +2,6 @@
module BulkImports
class ExportUploader < ImportExportUploader
- EXTENSION_WHITELIST = %w[ndjson.gz].freeze
+ EXTENSION_ALLOWLIST = %w[ndjson.gz].freeze
end
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 20aab58243a..bd959b14648 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -32,7 +32,14 @@ class FileUploader < GitlabUploader
def self.absolute_path(upload)
File.join(
- absolute_base_dir(upload.model),
+ root,
+ relative_path(upload)
+ )
+ end
+
+ def self.relative_path(upload)
+ File.join(
+ base_dir(upload.model),
upload.path # already contain the dynamic_segment, see #upload_path
)
end
diff --git a/app/uploaders/import_export_uploader.rb b/app/uploaders/import_export_uploader.rb
index 369afba2bae..7b161d72efb 100644
--- a/app/uploaders/import_export_uploader.rb
+++ b/app/uploaders/import_export_uploader.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
class ImportExportUploader < AttachmentUploader
- EXTENSION_WHITELIST = %w[tar.gz gz].freeze
+ EXTENSION_ALLOWLIST = %w[tar.gz gz].freeze
def self.workhorse_local_upload_path
File.join(options.storage_path, 'uploads', TMP_UPLOAD_PATH)
end
def extension_whitelist
- EXTENSION_WHITELIST
+ EXTENSION_ALLOWLIST
end
def move_to_cache
diff --git a/app/validators/gitlab/emoji_name_validator.rb b/app/validators/gitlab/emoji_name_validator.rb
new file mode 100644
index 00000000000..a9092d0194f
--- /dev/null
+++ b/app/validators/gitlab/emoji_name_validator.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# Gitlab::EmojiNameValidator
+#
+# Validates that the provided value matches an indexed emoji alpha code
+#
+# @example Usage
+# class AwardEmoji < ApplicationRecord
+# validate :name, 'gitlab/emoji_name': true
+# end
+module Gitlab
+ class EmojiNameValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless TanukiEmoji.find_by_alpha_code(value.to_s)
+ record.errors.add(attribute, (options[:message] || 'is not a valid emoji name'))
+ end
+ end
+ end
+end
diff --git a/app/validators/json_schemas/error_tracking_event_payload.json b/app/validators/json_schemas/error_tracking_event_payload.json
index 52efcf6800c..73ff71043ce 100644
--- a/app/validators/json_schemas/error_tracking_event_payload.json
+++ b/app/validators/json_schemas/error_tracking_event_payload.json
@@ -1,7 +1,7 @@
{
"description": "Error tracking event payload",
"type": "object",
- "required": [],
+ "required": ["exception"],
"properties": {
"environment": {
"type": "string"
@@ -14,7 +14,7 @@
},
"exception": {
"type": "object",
- "required": [],
+ "required": ["values"],
"properties": {
"values": {
"type": "array",
@@ -28,12 +28,6 @@
"value": {
"type": "string"
},
- "module": {
- "type": "string"
- },
- "thread_id": {
- "type": "number"
- },
"stacktrace": {
"type": "object",
"required": [],
@@ -44,9 +38,6 @@
"type": "object",
"required": [],
"properties": {
- "project_root": {
- "type": "string"
- },
"abs_path": {
"type": "string"
},
diff --git a/app/validators/json_schemas/helm_metadata.json b/app/validators/json_schemas/helm_metadata.json
index 7ac36e956f3..a5ff6f0b33a 100644
--- a/app/validators/json_schemas/helm_metadata.json
+++ b/app/validators/json_schemas/helm_metadata.json
@@ -103,7 +103,23 @@
"import-values": {
"type": "array",
"items": {
-
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "child": {
+ "type": "string"
+ },
+ "parent": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ ]
}
},
"alias": {
diff --git a/app/validators/json_schemas/npm_package_json.json b/app/validators/json_schemas/npm_package_json.json
new file mode 100644
index 00000000000..01bd874d214
--- /dev/null
+++ b/app/validators/json_schemas/npm_package_json.json
@@ -0,0 +1,26 @@
+{
+ "description": "NPM package json metadata",
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "version": { "type": "string" },
+ "dist": {
+ "type": "object",
+ "properties": {
+ "tarball": { "type": "string" },
+ "shasum": { "type": "string" }
+ },
+ "additionalProperties": true,
+ "required": [
+ "tarball",
+ "shasum"
+ ]
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "name",
+ "version",
+ "dist"
+ ]
+}
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
index ba7d747fa04..58ea2be8b61 100644
--- a/app/views/admin/application_settings/_performance_bar.html.haml
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -6,7 +6,7 @@
.form-check
= f.check_box :performance_bar_enabled, class: 'form-check-input', data: { qa_selector: 'enable_performance_bar_checkbox'}
= f.label :performance_bar_enabled, class: 'form-check-label' do
- = _("Allow non-administrators to access to the performance bar")
+ = _("Allow non-administrators access to the performance bar")
.form-group
= f.label :performance_bar_allowed_group_path, _('Allow access to members of the following group'), class: 'label-bold'
= f.text_field :performance_bar_allowed_group_path, class: 'form-control gl-form-input', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
diff --git a/app/views/admin/application_settings/_protected_paths.html.haml b/app/views/admin/application_settings/_protected_paths.html.haml
index 04b42f42014..d273c81f51d 100644
--- a/app/views/admin/application_settings/_protected_paths.html.haml
+++ b/app/views/admin/application_settings/_protected_paths.html.haml
@@ -2,30 +2,26 @@
= form_errors(@application_setting)
%fieldset
- - if omnibus_protected_paths_throttle?
- .bs-callout.bs-callout-danger
- - relative_url_link = 'https://docs.gitlab.com/ee/user/admin_area/settings/protected_paths.html#migrate-settings-from-gitlab-123-and-earlier'
- - relative_url_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: relative_url_link }
- = _("Omnibus Protected Paths throttle is active, and takes priority over these settings. From 12.4, Omnibus throttle is deprecated and will be removed in a future release. Please read the %{relative_url_link_start}Migrating Protected Paths documentation%{relative_url_link_end}.").html_safe % { relative_url_link_start: relative_url_link_start, relative_url_link_end: '</a>'.html_safe }
-
.form-group
.form-check
= f.check_box :throttle_protected_paths_enabled, class: 'form-check-input'
= f.label :throttle_protected_paths_enabled, class: 'form-check-label' do
- = _('Enable protected paths rate limit')
+ = _('Enable rate limiting for POST requests to the specified paths')
%span.form-text.text-muted
- = _('Helps reduce request volume for protected paths')
+ = _('Helps reduce request volume for protected paths.')
.form-group
- = f.label :throttle_protected_paths_requests_per_period, 'Max requests per period per user', class: 'label-bold'
+ = f.label :throttle_protected_paths_requests_per_period, 'Maximum requests per period per user', class: 'label-bold'
= f.number_field :throttle_protected_paths_requests_per_period, class: 'form-control gl-form-input'
.form-group
- = f.label :throttle_protected_paths_period_in_seconds, 'Rate limit period in seconds', class: 'label-bold'
+ = f.label :throttle_protected_paths_period_in_seconds, 'Rate limit period (in seconds)', class: 'label-bold'
= f.number_field :throttle_protected_paths_period_in_seconds, class: 'form-control gl-form-input'
.form-group
= f.label :protected_paths, class: 'label-bold' do
+ = _('Paths to protect with rate limiting')
+ = f.text_area :protected_paths_raw, placeholder: '/users/sign_in,/users/password', class: 'form-control gl-form-input', rows: 10
+ %span.form-text.text-muted
- relative_url_link = 'https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab'
- relative_url_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: relative_url_link }
- = _('All paths are relative to the GitLab URL. Do not include %{relative_url_link_start}relative URL%{relative_url_link_end}.').html_safe % { relative_url_link_start: relative_url_link_start, relative_url_link_end: '</a>'.html_safe }
- = f.text_area :protected_paths_raw, placeholder: '/users/sign_in,/users/password', class: 'form-control gl-form-input', rows: 10
+ = _('All paths are relative to the GitLab URL. Do not include %{relative_url_link_start}relative URLs%{relative_url_link_end}.').html_safe % { relative_url_link_start: relative_url_link_start, relative_url_link_end: '</a>'.html_safe }
= f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
diff --git a/app/views/admin/application_settings/_sentry.html.haml b/app/views/admin/application_settings/_sentry.html.haml
new file mode 100644
index 00000000000..5fd373d59e9
--- /dev/null
+++ b/app/views/admin/application_settings/_sentry.html.haml
@@ -0,0 +1,22 @@
+= form_for @application_setting, url: metrics_and_profiling_admin_application_settings_path(anchor: 'js-sentry-settings'), html: { class: 'fieldset-form', id: 'sentry-settings' } do |f|
+ = form_errors(@application_setting)
+
+ %span.text-muted
+ = _('Changing any setting here requires an application restart')
+
+ %fieldset
+ .form-group
+ .form-check
+ = f.check_box :sentry_enabled, class: 'form-check-input'
+ = f.label :sentry_enabled, _('Enable Sentry error tracking'), class: 'form-check-label'
+ .form-group
+ = f.label :sentry_dsn, _('DSN'), class: 'label-light'
+ = f.text_field :sentry_dsn, class: 'form-control gl-form-input', placeholder: 'https://public@sentry.example.com/1'
+ .form-group
+ = f.label :sentry_clientside_dsn, _('Clientside DSN'), class: 'label-light'
+ = f.text_field :sentry_clientside_dsn, class: 'form-control gl-form-input', placeholder: 'https://public@sentry.example.com/2'
+ .form-group
+ = f.label :sentry_environment, _('Environment'), class: 'label-light'
+ = f.text_field :sentry_environment, class: 'form-control gl-form-input', placeholder: Rails.env
+
+ = f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index 5bdad50c161..4fba1aee12d 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -15,7 +15,7 @@
.form-group
- can_be_configured = @application_setting.usage_ping_can_be_configured?
.form-check
- = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input'
+ = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input', data: { qa_selector: 'enable_usage_data_checkbox' }
= f.label :usage_ping_enabled, class: 'form-check-label' do
= _('Enable Service Ping')
.form-text.text-muted
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index b6266c3ea34..e56c898b236 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -1,13 +1,11 @@
-= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-visibility-settings'), html: { class: 'fieldset-form', id: 'visibility-settings' } do |f|
+= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-visibility-settings'), html: { class: 'fieldset-form', id: 'visibility-settings' } do |f|
= form_errors(@application_setting)
%fieldset
- = render 'shared/default_branch_protection', f: f, selected_level: @application_setting.default_branch_protection
+ = render 'shared/default_branch_protection', f: f
= render_if_exists 'admin/application_settings/group_owners_can_manage_default_branch_protection_setting', form: f
- .form-group
- = f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold'
- = f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control'
+ = render 'shared/project_creation_levels', f: f, method: :default_project_creation, legend: s_('ProjectCreationLevel|Default project creation protection')
= render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f
= render_if_exists 'admin/application_settings/default_delayed_project_deletion_setting', form: f
= render_if_exists 'admin/application_settings/default_project_deletion_adjourned_period_setting', form: f
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index a72c96bb577..53ba626760b 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -80,7 +80,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set the maximum session time for a web terminal.')
- = link_to _('How do I use a web terminal?'), help_page_path('ci/environments/index.md', anchor: 'web-terminals'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('How do I use a web terminal?'), help_page_path('ci/environments/index.md', anchor: 'web-terminals-deprecated'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'terminal'
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index 6087551d7c7..7cb5760f62a 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -42,7 +42,7 @@
.js-self-monitoring-settings{ data: self_monitoring_project_data }
-%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?) }
+%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'usage_statistics_settings_content' } }
.settings-header#usage-statistics
%h4
= _('Usage statistics')
@@ -54,3 +54,15 @@
= render 'usage'
= render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded_by_default?
+
+- if Feature.enabled?(:configure_sentry_in_application_settings, default_enabled: :yaml)
+ %section.settings.as-sentry.no-animate#js-sentry-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'sentry_settings_content' } }
+ .settings-header
+ %h4
+ = _('Sentry')
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Configure Sentry integration for error tracking')
+ .settings-content
+ = render 'sentry'
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 58e3f3f1136..3a053205725 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -87,14 +87,12 @@
%section.settings.as-protected-paths.no-animate#js-protected-paths-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
- = _('Protected Paths')
+ = _('Protected paths')
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Configure paths to be protected by Rack Attack.')
- .help-block
- = _('These paths are protected for POST requests.')
- = link_to _('More information'), help_page_path('security/rack_attack', anchor: 'protected-paths-throttle'), target: '_blank'
+ = _('Rate limit access to specified paths.')
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/protected_paths.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'protected_paths'
diff --git a/app/views/admin/application_settings/reporting.html.haml b/app/views/admin/application_settings/reporting.html.haml
index d2e118f0624..ae6243c3b50 100644
--- a/app/views/admin/application_settings/reporting.html.haml
+++ b/app/views/admin/application_settings/reporting.html.haml
@@ -13,6 +13,9 @@
.settings-content
= render 'spam'
+-# this partial is from JiHu, see details in https://gitlab.com/gitlab-jh/gitlab/-/merge_requests/135
+= render_if_exists 'admin/application_settings/content_validation_section'
+
%section.settings.as-abuse.no-animate#js-abuse-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 8dcd5f81c23..d9c683cbcc3 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -6,4 +6,5 @@
= render 'shared/doorkeeper/applications/show',
edit_path: edit_admin_application_path(@application),
delete_path: admin_application_path(@application),
+ index_path: admin_applications_path,
show_trusted_row: true
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 681e7ccb613..4197d5b961f 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -153,9 +153,9 @@
%span.float-right
#{Rails::VERSION::STRING}
%p
- = Gitlab::Database.main.human_adapter_name
+ = ApplicationRecord.database.human_adapter_name
%span.float-right
- = Gitlab::Database.main.version
+ = ApplicationRecord.database.version
%p
= _('Redis')
%span.float-right
diff --git a/app/views/admin/dashboard/stats.html.haml b/app/views/admin/dashboard/stats.html.haml
index b98d11b734b..e0701812ba3 100644
--- a/app/views/admin/dashboard/stats.html.haml
+++ b/app/views/admin/dashboard/stats.html.haml
@@ -1,74 +1,75 @@
- page_title s_('AdminArea|Users statistics')
-%h3.my-4
+%h3.gl-my-6
= s_('AdminArea|Users statistics')
%table.table.gl-text-gray-500
%tr
- %td.p-3
+ %td.gl-p-5!
= s_('AdminArea|Users without a Group and Project')
= render_if_exists 'admin/dashboard/included_free_in_license_tooltip'
- %td.p-3.text-right
- = @users_statistics&.without_groups_and_projects.to_i
+ %td.gl-text-right{ class: 'gl-p-5!' }
+ = @users_statistics&.without_groups_and_projects
+ = render_if_exists 'admin/dashboard/minimal_access_stats_row', users_statistics: @users_statistics
%tr
- %td.p-3
+ %td.gl-p-5!
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Guest')
= render_if_exists 'admin/dashboard/included_free_in_license_tooltip'
- %td.p-3.text-right
- = @users_statistics&.with_highest_role_guest.to_i
+ %td.gl-text-right{ class: 'gl-p-5!' }
+ = @users_statistics&.with_highest_role_guest
%tr
- %td.p-3
+ %td.gl-p-5!
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Reporter')
- %td.p-3.text-right
- = @users_statistics&.with_highest_role_reporter.to_i
+ %td.gl-text-right{ class: 'gl-p-5!' }
+ = @users_statistics&.with_highest_role_reporter
%tr
- %td.p-3
+ %td.gl-p-5!
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Developer')
- %td.p-3.text-right
- = @users_statistics&.with_highest_role_developer.to_i
+ %td.gl-text-right{ class: 'gl-p-5!' }
+ = @users_statistics&.with_highest_role_developer
%tr
- %td.p-3
+ %td.gl-p-5!
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Maintainer')
- %td.p-3.text-right
- = @users_statistics&.with_highest_role_maintainer.to_i
+ %td.gl-text-right{ class: 'gl-p-5!' }
+ = @users_statistics&.with_highest_role_maintainer
%tr
- %td.p-3
+ %td.gl-p-5!
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Owner')
- %td.p-3.text-right
- = @users_statistics&.with_highest_role_owner.to_i
+ %td.gl-text-right{ class: 'gl-p-5!' }
+ = @users_statistics&.with_highest_role_owner
%tr
- %td.p-3
+ %td.gl-p-5!
= s_('AdminArea|Bots')
- %td.p-3.text-right
- = @users_statistics&.bots.to_i
+ %td.gl-text-right{ class: 'gl-p-5!' }
+ = @users_statistics&.bots
= render_if_exists 'admin/dashboard/billable_users_row'
%tr.bg-gray-light.gl-text-gray-900
- %td.p-3
+ %td.gl-p-5!
%strong
= s_('AdminArea|Active users')
- %td.p-3.text-right
+ %td.gl-text-right{ class: 'gl-p-5!' }
%strong
- = @users_statistics&.active.to_i
+ = @users_statistics&.active
%tr.bg-gray-light.gl-text-gray-900
- %td.p-3
+ %td.gl-p-5!
%strong
= s_('AdminArea|Blocked users')
- %td.p-3.text-right
+ %td.gl-text-right{ class: 'gl-p-5!' }
%strong
- = @users_statistics&.blocked.to_i
+ = @users_statistics&.blocked
%tr.bg-gray-light.gl-text-gray-900
- %td.p-3
+ %td.gl-p-5!
%strong
= s_('AdminArea|Total users')
- %td.p-3.text-right
+ %td.gl-text-right{ class: 'gl-p-5!' }
%strong
- = @users_statistics&.total.to_i
+ = @users_statistics&.total
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index eec8f816f04..ba4abdc02e4 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -1,33 +1,44 @@
- page_title _('Deploy Keys')
-- if @deploy_keys.any?
- %h3.page-title.deploy-keys-title
- = _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.load.size }
- = link_to _('New deploy key'), new_admin_deploy_key_path, class: 'float-right btn gl-button btn-confirm btn-md gl-button'
- .table-holder.deploy-keys-list
- %table.table
+
+- if Feature.enabled?(:admin_deploy_keys_vue, default_enabled: :yaml)
+ #js-admin-deploy-keys-table{ data: admin_deploy_keys_data }
+- else
+ - if @deploy_keys.any?
+ %h3.page-title.deploy-keys-title
+ = _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.load.size }
+ = link_to _('New deploy key'), new_admin_deploy_key_path, class: 'float-right btn gl-button btn-confirm btn-md gl-button'
+ %table.table.b-table.gl-table.b-table-stacked-lg{ data: { testid: 'deploy-keys-list' } }
%thead
%tr
- %th.col-sm-2= _('Title')
- %th.col-sm-4= _('Fingerprint')
- %th.col-sm-2= _('Projects with write access')
- %th.col-sm-2= _('Added at')
- %th.col-sm-2
+ %th= _('Title')
+ %th= _('Fingerprint')
+ %th= _('Projects with write access')
+ %th= _('Created')
+ %th.gl-lg-w-1px.gl-white-space-nowrap
+ %span.gl-sr-only
+ = _('Actions')
%tbody
- @deploy_keys.each do |deploy_key|
%tr
- %td
- %strong= deploy_key.title
- %td
- %code.key-fingerprint= deploy_key.fingerprint
- %td
- - deploy_key.projects_with_write_access.each do |project|
- = link_to project.full_name, admin_project_path(project), class: 'label deploy-project-label'
- %td
- %span.cgray
- = _('added %{created_at_timeago}').html_safe % { created_at_timeago: time_ago_with_tooltip(deploy_key.created_at) }
- %td
- .float-right
- = link_to _('Edit'), edit_admin_deploy_key_path(deploy_key), class: 'btn gl-button btn-sm'
- = link_to _('Remove'), admin_deploy_key_path(deploy_key), data: { confirm: _('Are you sure?') }, method: :delete, class: 'gl-button btn btn-sm btn-danger delete-key'
-- else
- = render 'shared/empty_states/deploy_keys'
+ %td{ data: { label: _('Title') } }
+ %div
+ = deploy_key.title
+ %td{ data: { label: _('Fingerprint') } }
+ %div
+ %code= deploy_key.fingerprint
+ %td{ data: { label: _('Projects with write access') } }
+ %div
+ - deploy_key.projects_with_write_access.each do |project|
+ = link_to project.full_name, admin_project_path(project), class: 'gl-display-block'
+ %td{ data: { label: _('Created') } }
+ %div
+ = time_ago_with_tooltip(deploy_key.created_at)
+ %td.gl-lg-w-1px.gl-white-space-nowrap{ data: { label: _('Actions') } }
+ %div
+ = link_to edit_admin_deploy_key_path(deploy_key), class: 'btn btn-default btn-md gl-button btn-icon gl-mr-3', aria: { label: _('Edit deploy key') } do
+ = sprite_icon('pencil', css_class: 'gl-button-icon')
+ = link_to admin_deploy_key_path(deploy_key), data: { confirm: _('Are you sure?') }, method: :delete, class: 'btn btn-danger btn-md gl-button btn-icon', aria: { label: _('Remove deploy key') } do
+ = sprite_icon('remove', css_class: 'gl-button-icon')
+
+ - else
+ = render 'shared/empty_states/deploy_keys'
diff --git a/app/views/admin/dev_ops_report/_report.html.haml b/app/views/admin/dev_ops_report/_score.html.haml
index 208afefc73b..208afefc73b 100644
--- a/app/views/admin/dev_ops_report/_report.html.haml
+++ b/app/views/admin/dev_ops_report/_score.html.haml
diff --git a/app/views/admin/dev_ops_report/show.html.haml b/app/views/admin/dev_ops_report/show.html.haml
index c16ef7af76d..a2425b93ad3 100644
--- a/app/views/admin/dev_ops_report/show.html.haml
+++ b/app/views/admin/dev_ops_report/show.html.haml
@@ -1,4 +1,4 @@
-- page_title _('DevOps Report')
+- page_title _('DevOps Reports')
- add_page_specific_style 'page_bundles/dev_ops_report'
.container
@@ -6,5 +6,5 @@
- if show_adoption?
= render_if_exists 'admin/dev_ops_report/devops_tabs'
- else
- = render 'report'
+ = render 'score'
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index f298fce7bcf..0539d7f13a2 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -1,4 +1,4 @@
- breadcrumb_title _('Runners')
- page_title _('Runners')
-#js-admin-runners{ data: { registration_token: Gitlab::CurrentSettings.runners_registration_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', active_runners_count: @active_runners_count } }
+#js-admin-runners{ data: admin_runners_data_attributes }
diff --git a/app/views/admin/topics/_topic.html.haml b/app/views/admin/topics/_topic.html.haml
index abf3cffa422..959e7ab31fc 100644
--- a/app/views/admin/topics/_topic.html.haml
+++ b/app/views/admin/topics/_topic.html.haml
@@ -6,7 +6,7 @@
.gl-min-w-0.gl-flex-grow-1
.title
- = topic.name
+ = link_to topic.name, topic_explore_projects_path(topic_name: topic.name)
.stats.gl-text-gray-500.gl-flex-shrink-0.gl-display-none.gl-sm-display-flex
%span.gl-ml-5.has-tooltip{ title: n_('%d project', '%d projects', topic.total_projects_count) % topic.total_projects_count }
diff --git a/app/views/admin/users/_tabs.html.haml b/app/views/admin/users/_tabs.html.haml
index 90f06eeaf3f..6c14e1189fe 100644
--- a/app/views/admin/users/_tabs.html.haml
+++ b/app/views/admin/users/_tabs.html.haml
@@ -1,7 +1,3 @@
-%ul.nav-links.nav-tabs.nav.js-users-tabs{ role: 'tablist' }
- %li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: admin_users_path, class: active_when(current_page?(admin_users_path)), role: 'tab' }
- = s_('AdminUsers|Users')
- %li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: admin_cohorts_path, class: active_when(current_page?(admin_cohorts_path)), role: 'tab' }
- = s_('AdminUsers|Cohorts')
+= gl_tabs_nav({ class: 'js-users-tabs' }) do
+ = gl_tab_link_to s_('AdminUsers|Users'), admin_users_path
+ = gl_tab_link_to s_('AdminUsers|Cohorts'), admin_cohorts_path
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index 1a43d91b800..e62e4cfa192 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -3,48 +3,38 @@
= sprite_icon('chevron-lg-left', size: 12)
.fade-right
= sprite_icon('chevron-lg-right', size: 12)
- %ul.nav-links.nav.nav-tabs.scrolling-tabs
- = nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
- = link_to admin_users_path do
- = s_('AdminUsers|Active')
- %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.active_without_ghosts)
- = nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
- = link_to admin_users_path(filter: "admins") do
- = s_('AdminUsers|Admins')
- %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.admins)
- = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
- = link_to admin_users_path(filter: 'two_factor_enabled') do
- = s_('AdminUsers|2FA Enabled')
- %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.with_two_factor)
- = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
- = link_to admin_users_path(filter: 'two_factor_disabled') do
- = s_('AdminUsers|2FA Disabled')
- %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.without_two_factor)
- = nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
- = link_to admin_users_path(filter: 'external') do
- = s_('AdminUsers|External')
- %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.external)
- = nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
- = link_to admin_users_path(filter: "blocked") do
- = s_('AdminUsers|Blocked')
- %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.blocked)
+ = gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1' }) do
+ = gl_tab_link_to admin_users_path, { item_active: active_when(params[:filter].nil?), class: 'gl-border-0!' } do
+ = s_('AdminUsers|Active')
+ = gl_tab_counter_badge(limited_counter_with_delimiter(User.active_without_ghosts))
+ = gl_tab_link_to admin_users_path(filter: "admins"), { item_active: active_when(params[:filter] == 'admins'), class: 'gl-border-0!' } do
+ = s_('AdminUsers|Admins')
+ = gl_tab_counter_badge(limited_counter_with_delimiter(User.admins))
+ = gl_tab_link_to admin_users_path(filter: 'two_factor_enabled'), { item_active: active_when(params[:filter] == 'two_factor_enabled'), class: 'filter-two-factor-enabled gl-border-0!' } do
+ = s_('AdminUsers|2FA Enabled')
+ = gl_tab_counter_badge(limited_counter_with_delimiter(User.with_two_factor))
+ = gl_tab_link_to admin_users_path(filter: 'two_factor_disabled'), { item_active: active_when(params[:filter] == 'two_factor_disabled'), class: 'filter-two-factor-disabled gl-border-0!' } do
+ = s_('AdminUsers|2FA Disabled')
+ = gl_tab_counter_badge(limited_counter_with_delimiter(User.without_two_factor))
+ = gl_tab_link_to admin_users_path(filter: 'external'), { item_active: active_when(params[:filter] == 'external'), class: 'gl-border-0!' } do
+ = s_('AdminUsers|External')
+ = gl_tab_counter_badge(limited_counter_with_delimiter(User.external))
+ = gl_tab_link_to admin_users_path(filter: "blocked"), { item_active: active_when(params[:filter] == 'blocked'), class: 'gl-border-0!' } do
+ = s_('AdminUsers|Blocked')
+ = gl_tab_counter_badge(limited_counter_with_delimiter(User.blocked))
- if ban_feature_available?
- = nav_link(html_options: { class: active_when(params[:filter] == 'banned') }) do
- = link_to admin_users_path(filter: "banned") do
- = s_('AdminUsers|Banned')
- %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.banned)
- = nav_link(html_options: { class: "#{active_when(params[:filter] == 'blocked_pending_approval')} filter-blocked-pending-approval" }) do
- = link_to admin_users_path(filter: "blocked_pending_approval"), data: { qa_selector: 'pending_approval_tab' } do
- = s_('AdminUsers|Pending approval')
- %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.blocked_pending_approval)
- = nav_link(html_options: { class: active_when(params[:filter] == 'deactivated') }) do
- = link_to admin_users_path(filter: "deactivated") do
- = s_('AdminUsers|Deactivated')
- %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.deactivated)
- = nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
- = link_to admin_users_path(filter: "wop") do
- = s_('AdminUsers|Without projects')
- %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.without_projects)
+ = gl_tab_link_to admin_users_path(filter: "banned"), { item_active: active_when(params[:filter] == 'banned'), class: 'gl-border-0!' } do
+ = s_('AdminUsers|Banned')
+ = gl_tab_counter_badge(limited_counter_with_delimiter(User.banned))
+ = gl_tab_link_to admin_users_path(filter: "blocked_pending_approval"), { item_active: active_when(params[:filter] == 'blocked_pending_approval'), class: 'filter-blocked-pending-approval gl-border-0!', data: { qa_selector: 'pending_approval_tab' } } do
+ = s_('AdminUsers|Pending approval')
+ = gl_tab_counter_badge(limited_counter_with_delimiter(User.blocked_pending_approval))
+ = gl_tab_link_to admin_users_path(filter: "deactivated"), { item_active: active_when(params[:filter] == 'deactivated'), class: 'gl-border-0!' } do
+ = s_('AdminUsers|Deactivated')
+ = gl_tab_counter_badge(limited_counter_with_delimiter(User.deactivated))
+ = gl_tab_link_to admin_users_path(filter: "wop"), { item_active: active_when(params[:filter] == 'wop'), class: 'gl-border-0!' } do
+ = s_('AdminUsers|Without projects')
+ = gl_tab_counter_badge(limited_counter_with_delimiter(User.without_projects))
.nav-controls
= render_if_exists 'admin/users/admin_email_users'
= render_if_exists 'admin/users/admin_export_user_permissions'
diff --git a/app/views/clusters/clusters/_cluster_list.html.haml b/app/views/clusters/clusters/_cluster_list.html.haml
index 38ed7e334c9..e5e1b68225e 100644
--- a/app/views/clusters/clusters/_cluster_list.html.haml
+++ b/app/views/clusters/clusters/_cluster_list.html.haml
@@ -1,12 +1,10 @@
-- if clusters.empty?
- = render 'empty_state'
-- else
+- if !clusters.empty?
.top-area.adjust
.gl-display-block.gl-text-right.gl-my-4.gl-w-full
- if clusterable.can_add_cluster?
- = link_to s_('ClusterIntegration|Connect cluster with certificate'), clusterable.new_path, class: 'btn gl-button btn-confirm js-add-cluster gl-py-2', qa_selector: :integrate_kubernetes_cluster_button
+ = link_to s_('ClusterIntegration|Connect cluster with certificate'), clusterable.new_path, class: 'btn gl-button btn-confirm js-add-cluster gl-py-2', data: { qa_selector: 'integrate_kubernetes_cluster_button' }
- else
%span.btn.gl-button.btn-confirm.js-add-cluster.disabled.gl-py-2
= s_("ClusterIntegration|Connect cluster with certificate")
- #js-clusters-list-app{ data: js_clusters_list_data(clusterable.index_path(format: :json)) }
+#js-clusters-list-app{ data: js_clusters_list_data(clusterable) }
diff --git a/app/views/clusters/clusters/_empty_state.html.haml b/app/views/clusters/clusters/_empty_state.html.haml
deleted file mode 100644
index feef3e0027f..00000000000
--- a/app/views/clusters/clusters/_empty_state.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-.row.empty-state
- .col-12
- .svg-content= image_tag 'illustrations/clusters_empty.svg'
- .col-12
- .text-content
- %h4.gl-text-center= s_('ClusterIntegration|Integrate Kubernetes with a cluster certificate')
- %p.gl-text-center
- = s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
- = clusterable.empty_state_help_text
- = clusterable.learn_more_link
-
- - if clusterable.can_add_cluster?
- .gl-text-center
- = link_to s_('ClusterIntegration|Integrate with a cluster certificate'), clusterable.new_path, class: 'gl-button btn btn-confirm', data: { qa_selector: 'add_kubernetes_cluster_link' }
diff --git a/app/views/clusters/clusters/aws/_new.html.haml b/app/views/clusters/clusters/aws/_new.html.haml
index f6d50410e9a..7142dd83dce 100644
--- a/app/views/clusters/clusters/aws/_new.html.haml
+++ b/app/views/clusters/clusters/aws/_new.html.haml
@@ -11,7 +11,7 @@
'external-id' => @aws_role.role_external_id,
'role-arn' => @aws_role.role_arn,
'instance-types' => @instance_types,
- 'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index'),
+ 'kubernetes-integration-help-path' => help_page_path('user/infrastructure/clusters/index.md'),
'account-and-external-ids-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'how-to-create-a-new-cluster-on-eks-through-cluster-certificates-deprecated'),
'create-role-arn-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'how-to-create-a-new-cluster-on-eks-through-cluster-certificates-deprecated'),
'external-link-icon' => sprite_icon('external-link') } }
diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml
index 5266fad9278..173456926a5 100644
--- a/app/views/clusters/clusters/gcp/_form.html.haml
+++ b/app/views/clusters/clusters/gcp/_form.html.haml
@@ -2,7 +2,7 @@
- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones'
- machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types'
- pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype'
-- kubernetes_integration_url = help_page_path('user/project/clusters/index')
+- kubernetes_integration_url = help_page_path('user/infrastructure/clusters/index.md')
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon }
@@ -74,7 +74,7 @@
label_class: 'label-bold' }
.form-text.text-muted
= s_('ClusterIntegration|Allow GitLab to manage namespaces and service accounts for this cluster.')
- = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters.md', anchor: 'gitlab-managed-clusters'), target: '_blank'
+ = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank'
.form-group
= field.check_box :namespace_per_environment, { label: s_('ClusterIntegration|Namespace per environment'), label_class: 'label-bold' }
diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index 45287a01cc9..457e34b306a 100644
--- a/app/views/clusters/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
@@ -3,24 +3,10 @@
= render_gcp_signup_offer
-.clusters-container.gl-my-2
+.clusters-container
- if display_cluster_agents?(clusterable)
- .js-toggle-container
- %ul.nav-links.nav-tabs.nav{ role: 'tablist' }
- %li.nav-item{ role: 'presentation' }
- %a.nav-link.active{ href: "#certificate-clusters-pane", id: "certificate-clusters-tab", data: { toggle: 'tab' }, role: 'tab' }
- %span= s_('ClusterIntegration|Clusters connected with a certificate')
-
- %li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: "#agent-clusters-pane", id: "agent-clusters-tab", data: { toggle: 'tab' }, role: 'tab' }
- %span= s_('ClusterIntegration|GitLab Agent managed clusters')
-
- .tab-content
- .tab-pane.active{ id: 'certificate-clusters-pane', role: 'tabpanel' }
- = render 'cluster_list', clusters: @clusters
-
- .tab-pane{ id: 'agent-clusters-pane', role: 'tabpanel' }
- #js-cluster-agents-list{ data: js_cluster_agents_list_data(clusterable) }
+ .gl-my-6
+ .js-clusters-main-view{ data: js_clusters_data(clusterable) }
- else
= render 'cluster_list', clusters: @clusters
diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml
index 74e336723ba..7af7a812338 100644
--- a/app/views/clusters/clusters/new.html.haml
+++ b/app/views/clusters/clusters/new.html.haml
@@ -1,31 +1,31 @@
- breadcrumb_title _('Kubernetes')
- page_title _('Kubernetes Cluster')
-- active_tab = local_assigns.fetch(:active_tab, 'create')
- provider = params[:provider]
+- active_tab = params[:tab] || local_assigns.fetch(:active_tab, 'create')
+- is_active_tab_create = active_tab === 'create'
+- is_active_tab_add = active_tab === 'add'
= render_gcp_signup_offer
.row.gl-mt-3
.col-md-3
= render 'sidebar'
- .col-md-9.js-toggle-container
- %ul.nav-links.nav-tabs.gitlab-tabs.nav{ role: 'tablist' }
- %li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#create-cluster-pane', id: 'create-cluster-tab', class: active_when(active_tab == 'create'), data: { toggle: 'tab' }, role: 'tab' }
- %span
- = create_new_cluster_label(provider: params[:provider])
- %li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#add-cluster-pane', id: 'add-cluster-tab', class: active_when(active_tab == 'add'), data: { toggle: 'tab', qa_selector: 'add_existing_cluster_tab' }, role: 'tab' }
- %span= s_('ClusterIntegration|Connect existing cluster')
+ .col-md-9
+ = gl_tabs_nav({ class: 'nav-justified' }) do
+ = gl_tab_link_to clusterable.new_path(tab: 'create'), { item_active: is_active_tab_create } do
+ %span= create_new_cluster_label(provider: params[:provider])
+ = gl_tab_link_to s_('ClusterIntegration|Connect existing cluster'), clusterable.new_path(tab: 'add'), { item_active: is_active_tab_add, qa_selector: 'add_existing_cluster_tab' }
- .tab-content.gitlab-tab-content
- .tab-pane.p-0{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
- = render 'clusters/clusters/cloud_providers/cloud_provider_selector'
+ .tab-content
+ - if is_active_tab_create
+ .tab-pane.active{ role: 'tabpanel' }
+ = render 'clusters/clusters/cloud_providers/cloud_provider_selector'
- - if ['aws', 'gcp'].include?(provider)
- .p-3.border-top
- = render "clusters/clusters/#{provider}/new"
+ - if ['aws', 'gcp'].include?(provider)
+ .p-3.border-top
+ = render "clusters/clusters/#{provider}/new"
- .tab-pane{ id: 'add-cluster-pane', class: active_when(active_tab == 'add'), role: 'tabpanel' }
- #js-cluster-new{ data: js_cluster_new }
- = render 'clusters/clusters/user/form'
+ - if is_active_tab_add
+ .tab-pane.active.gl-p-5{ role: 'tabpanel' }
+ #js-cluster-new{ data: js_cluster_new }
+ = render 'clusters/clusters/user/form'
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 2a09d8d8cc0..e4c8f225ed2 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -13,7 +13,7 @@
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
provider_type: @cluster.provider_type,
- help_path: help_page_path('user/project/clusters/index.md'),
+ help_path: help_page_path('user/infrastructure/clusters/index.md'),
environments_help_path: help_page_path('ci/environments/index.md', anchor: 'create-a-static-environment'),
clusters_help_path: help_page_path('user/project/clusters/deploy_to_cluster.md'),
deploy_boards_help_path: help_page_path('user/project/deploy_boards.md', anchor: 'enabling-deploy-boards'),
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index fdaf2107686..b94b14bf6bd 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -14,19 +14,7 @@
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
- %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs{ class: ('border-0' if feature_project_list_filter_bar) }
- = nav_link(page: [dashboard_projects_path, root_path]) do
- = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
- = _("Your projects")
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_user_projects_count)
- = nav_link(page: starred_dashboard_projects_path) do
- = link_to starred_dashboard_projects_path, data: {placement: 'right'} do
- = _("Starred projects")
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_starred_projects_count)
- = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
- = link_to explore_root_path, data: {placement: 'right'} do
- = _("Explore projects")
- = render_if_exists "dashboard/removed_projects_tab", removed_projects_count: @removed_projects_count
+ = render 'dashboard/projects_nav'
- unless feature_project_list_filter_bar
.nav-controls
= render 'shared/projects/search_form'
diff --git a/app/views/dashboard/_projects_nav.html.haml b/app/views/dashboard/_projects_nav.html.haml
new file mode 100644
index 00000000000..64aa1e01d06
--- /dev/null
+++ b/app/views/dashboard/_projects_nav.html.haml
@@ -0,0 +1,13 @@
+- is_your_projects_path = current_page?(dashboard_projects_path) || current_page?(root_path)
+- is_explore_projects_path = current_page?(explore_root_path) || current_page?(trending_explore_projects_path) || current_page?(starred_explore_projects_path) || current_page?(explore_projects_path)
+
+= gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do
+ = gl_tab_link_to dashboard_projects_path, { item_active: is_your_projects_path, class: 'shortcuts-activity', data: { placement: 'right' } } do
+ = _("Your projects")
+ = gl_tab_counter_badge(limited_counter_with_delimiter(@total_user_projects_count))
+ = gl_tab_link_to starred_dashboard_projects_path, { data: { placement: 'right' } } do
+ = _("Starred projects")
+ = gl_tab_counter_badge(limited_counter_with_delimiter(@total_starred_projects_count))
+ = gl_tab_link_to _("Explore projects"), explore_root_path, { item_active: is_explore_projects_path, data: { placement: 'right' } }
+ = gl_tab_link_to _("Explore topics"), topics_explore_projects_path, { data: { placement: 'right' } }
+ = render_if_exists "dashboard/removed_projects_tab", removed_projects_count: @removed_projects_count
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index e96b5695ddc..8c468812e33 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -7,10 +7,6 @@
= link_to _("New snippet"), new_snippet_path, class: "gl-button btn btn-confirm", title: _("New snippet")
.top-area
- %ul.nav-links.nav.nav-tabs
- = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
- = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
- Your snippets
- = nav_link(page: explore_snippets_path) do
- = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
- Explore snippets
+ = gl_tabs_nav({ class: 'gl-border-0' }) do
+ = gl_tab_link_to _('Your snippets'), dashboard_snippets_path, { title: _('Your snippets') }
+ = gl_tab_link_to _('Explore snippets'), explore_snippets_path, { title: _('Explore snippets') }
diff --git a/app/views/dashboard/projects/_nav.html.haml b/app/views/dashboard/projects/_nav.html.haml
index f9b61bf1f3e..3e39872902d 100644
--- a/app/views/dashboard/projects/_nav.html.haml
+++ b/app/views/dashboard/projects/_nav.html.haml
@@ -14,8 +14,6 @@
= link_to s_('DashboardProjects|All'), dashboard_projects_path, class: params[:personal].present? ? inactive_class : active_class
= link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true), class: params[:personal].present? ? active_class : inactive_class
- else
- %ul.nav-links.mobile-separator.nav.nav-tabs
- = nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
- = link_to s_('DashboardProjects|All'), dashboard_projects_path
- = nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
- = link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true)
+ = gl_tabs_nav do
+ = gl_tab_link_to s_('DashboardProjects|All'), dashboard_projects_path, { item_active: params[:personal].blank? }
+ = gl_tab_link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true), { item_active: params[:personal].present? }
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 58f817bf63b..aa54a1e589e 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -10,19 +10,13 @@
- if current_user.todos.any?
.top-area
- %ul.nav-links.mobile-separator.nav.nav-tabs
- %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
- = link_to todos_filter_path(state: 'pending') do
- %span
- To Do
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
- = number_with_delimiter(todos_pending_count)
- %li.todos-done{ class: active_when(params[:state] == 'done') }>
- = link_to todos_filter_path(state: 'done') do
- %span
- Done
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
- = number_with_delimiter(todos_done_count)
+ = gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do
+ = gl_tab_link_to todos_filter_path(state: 'pending'), item_active: params[:state].blank? || params[:state] == 'pending', class: "js-todos-pending" do
+ = _("To Do")
+ = gl_tab_counter_badge number_with_delimiter(todos_pending_count)
+ = gl_tab_link_to todos_filter_path(state: 'done'), item_active: params[:state] == 'done', class: "js-todos-done" do
+ = _("Done")
+ = gl_tab_counter_badge number_with_delimiter(todos_done_count)
.nav-controls
- if @allowed_todos.any?(&:pending?)
diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml
index 51354618aa4..eee223ff63c 100644
--- a/app/views/devise/confirmations/new.html.haml
+++ b/app/views/devise/confirmations/new.html.haml
@@ -7,7 +7,12 @@
.form-group
= f.label :email
= f.email_field :email, class: "form-control gl-form-input", required: true, title: _('Please provide a valid email address.'), value: nil
- .clearfix
+
+ %div
+ - if recaptcha_enabled?
+ = recaptcha_tags nonce: content_security_policy_nonce
+
+ .gl-mt-5
= f.submit _("Resend"), class: 'gl-button btn btn-confirm'
.clearfix.prepend-top-20
diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml
index 7f6ce712af2..7bbde4a39c7 100644
--- a/app/views/devise/passwords/new.html.haml
+++ b/app/views/devise/passwords/new.html.haml
@@ -8,7 +8,12 @@
= f.email_field :email, class: "form-control gl-form-input", required: true, value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.')
.form-text.text-muted
= _('Requires your primary GitLab email address.')
- .clearfix
+
+ %div
+ - if recaptcha_enabled?
+ = recaptcha_tags nonce: content_security_policy_nonce
+
+ .gl-mt-5
= f.submit _("Reset password"), class: "gl-button btn-confirm btn"
.clearfix.prepend-top-20
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index 75521d42f7e..3a568421ce9 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -6,4 +6,7 @@
%h3.page-title
= _("Application: %{name}") % { name: @application.name }
-= render 'shared/doorkeeper/applications/show', edit_path: edit_oauth_application_path(@application), delete_path: oauth_application_path(@application)
+= render 'shared/doorkeeper/applications/show',
+ edit_path: edit_oauth_application_path(@application),
+ delete_path: oauth_application_path(@application),
+ index_path: oauth_applications_path
diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml
index 0e9041d07ea..e114e4609f8 100644
--- a/app/views/errors/omniauth_error.html.haml
+++ b/app/views/errors/omniauth_error.html.haml
@@ -1,7 +1,7 @@
- content_for(:title, 'Auth Error')
.container
- = render "shared/errors/graphic_422.svg"
+ = render partial: "shared/errors/graphic_422", formats: :svg
%h3 Sign-in using #{@provider} auth failed
%p.light.subtitle Sign-in failed because #{@error}.
diff --git a/app/views/explore/groups/_nav.html.haml b/app/views/explore/groups/_nav.html.haml
index c337149a2f3..3c9c4e9f76b 100644
--- a/app/views/explore/groups/_nav.html.haml
+++ b/app/views/explore/groups/_nav.html.haml
@@ -1,8 +1,6 @@
.top-area
- %ul.nav-links.nav.nav-tabs
- = nav_link(page: explore_groups_path) do
- = link_to explore_groups_path do
- = _("Explore Groups")
+ = gl_tabs_nav({ class: 'gl-display-flex gl-flex-grow-1 gl-border-none'}) do
+ = gl_tab_link_to _("Explore Groups"), explore_groups_path
.nav-controls
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml
index 65b7d055843..9d7a6f1ccfb 100644
--- a/app/views/explore/projects/_nav.html.haml
+++ b/app/views/explore/projects/_nav.html.haml
@@ -1,14 +1,8 @@
.top-area
- %ul.nav-links.nav.nav-tabs
- = nav_link(page: [explore_projects_path, explore_root_path]) do
- = link_to explore_projects_path do
- = _('All')
- = nav_link(page: starred_explore_projects_path) do
- = link_to starred_explore_projects_path do
- = _('Most stars')
- = nav_link(page: trending_explore_projects_path) do
- = link_to trending_explore_projects_path do
- = _('Trending')
+ = gl_tabs_nav({ class: 'gl-display-flex gl-flex-grow-1 gl-border-none'}) do
+ = gl_tab_link_to _('All'), explore_projects_path, { item_active: current_page?(explore_projects_path) || current_page?(explore_root_path) }
+ = gl_tab_link_to _('Most stars'), starred_explore_projects_path
+ = gl_tab_link_to _('Trending'), trending_explore_projects_path
.nav-controls
- unless current_user
diff --git a/app/views/explore/projects/topic.html.haml b/app/views/explore/projects/topic.html.haml
new file mode 100644
index 00000000000..aeb040ea61f
--- /dev/null
+++ b/app/views/explore/projects/topic.html.haml
@@ -0,0 +1,31 @@
+- @hide_top_links = false
+- @no_container = true
+- page_title @topic.name, _("Topics")
+- max_topic_name_length = 50
+
+= render_dashboard_ultimate_trial(current_user)
+
+.gl-text-center.gl-bg-gray-10.gl-pb-2.gl-pt-6
+ .gl-pb-5.gl-align-items-center.gl-justify-content-center.gl-display-flex
+ .avatar-container.rect-avatar.s60.gl-flex-shrink-0
+ = topic_icon(@topic, alt: _('Topic avatar'), class: 'avatar topic-avatar s60')
+ - if @topic.name.length > max_topic_name_length
+ %h1.gl-mt-3.str-truncated.has-tooltip{ title: @topic.name }
+ = truncate(@topic.name, length: max_topic_name_length)
+ - else
+ %h1.gl-mt-3
+ = @topic.name
+ - if @topic.description.present?
+ .topic-description.gl-ml-4.gl-mr-4
+ = markdown(@topic.description)
+
+%div{ class: container_class }
+ .gl-py-5.gl-border-gray-100.gl-border-b-solid.gl-border-b-1
+ %h3.gl-m-0= _('Projects with this topic')
+ .top-area.gl-pt-2.gl-pb-2
+ .nav-controls
+ = render 'shared/projects/search_form'
+ = render 'shared/projects/dropdown'
+ = render 'filter'
+
+ = render 'projects', projects: @projects
diff --git a/app/views/explore/projects/topics.html.haml b/app/views/explore/projects/topics.html.haml
new file mode 100644
index 00000000000..228304d25b6
--- /dev/null
+++ b/app/views/explore/projects/topics.html.haml
@@ -0,0 +1,12 @@
+- @hide_top_links = true
+- page_title _("Topics")
+- header_title _("Topics"), topics_explore_projects_path
+
+= render_dashboard_ultimate_trial(current_user)
+
+- if current_user
+ = render 'explore/topics/head'
+- else
+ = render 'explore/head'
+
+= render partial: 'shared/topics/list'
diff --git a/app/views/explore/topics/_head.html.haml b/app/views/explore/topics/_head.html.haml
new file mode 100644
index 00000000000..f5ee95b16c3
--- /dev/null
+++ b/app/views/explore/topics/_head.html.haml
@@ -0,0 +1,9 @@
+.page-title-holder.d-flex.align-items-center
+ %h1.page-title= _('Projects')
+
+.top-area.scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= sprite_icon('chevron-lg-left', size: 12)
+ .fade-right= sprite_icon('chevron-lg-right', size: 12)
+ = render 'dashboard/projects_nav'
+ .nav-controls
+ = render 'shared/topics/search_form'
diff --git a/app/views/groups/_invite_members_side_nav_link.html.haml b/app/views/groups/_invite_members_side_nav_link.html.haml
new file mode 100644
index 00000000000..bccfa9897da
--- /dev/null
+++ b/app/views/groups/_invite_members_side_nav_link.html.haml
@@ -0,0 +1,8 @@
+.js-invite-members-trigger{ data: { trigger_source: 'group-side-nav',
+ classes: 'gl-text-decoration-none! gl-shadow-none! gl-text-body!',
+ icon: 'users',
+ display_text: title,
+ trigger_element: 'side-nav'} }
+
+= render partial: 'shared/nav/sidebar_submenu', locals: { sidebar_menu: sidebar_menu }
+= render 'groups/invite_members_modal', group: group
diff --git a/app/views/groups/boards/show.html.haml b/app/views/groups/boards/show.html.haml
index dbbf78eed00..92838fa4b11 100644
--- a/app/views/groups/boards/show.html.haml
+++ b/app/views/groups/boards/show.html.haml
@@ -1,3 +1 @@
-= render 'shared/alerts/positioning_disabled'
-
= render "shared/boards/show", board: @board, group: true
diff --git a/app/views/groups/crm/contacts.html.haml b/app/views/groups/crm/contacts.html.haml
new file mode 100644
index 00000000000..c452a969d17
--- /dev/null
+++ b/app/views/groups/crm/contacts.html.haml
@@ -0,0 +1,4 @@
+- breadcrumb_title _('Customer Relations Contacts')
+- page_title _('Customer Relations Contacts')
+
+#js-crm-contacts-app{ data: { group_full_path: @group.full_path } }
diff --git a/app/views/groups/crm/organizations.html.haml b/app/views/groups/crm/organizations.html.haml
new file mode 100644
index 00000000000..e83dab9fda6
--- /dev/null
+++ b/app/views/groups/crm/organizations.html.haml
@@ -0,0 +1,4 @@
+- breadcrumb_title _('Customer Relations Organizations')
+- page_title _('Customer Relations Organizations')
+
+#js-crm-organizations-app{ data: { group_full_path: @group.full_path } }
diff --git a/app/views/groups/dependency_proxies/show.html.haml b/app/views/groups/dependency_proxies/show.html.haml
index 8936c4dcbb4..47caec717af 100644
--- a/app/views/groups/dependency_proxies/show.html.haml
+++ b/app/views/groups/dependency_proxies/show.html.haml
@@ -1,5 +1,7 @@
- page_title _("Dependency Proxy")
+- @content_class = "limit-container-width" unless fluid_layout
- dependency_proxy_available = Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true) || @group.public?
#js-dependency-proxy{ data: { group_path: @group.full_path,
- dependency_proxy_available: dependency_proxy_available.to_s } }
+ dependency_proxy_available: dependency_proxy_available.to_s,
+ no_manifests_illustration: image_path('illustrations/docker-empty-state.svg') } }
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 0f11ca5fb8f..e55af71022e 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -10,7 +10,7 @@
.row{ 'v-cloak': true }
#create-group-pane.tab-pane
- = form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f|
+ = form_for @group, html: { class: 'group-form gl-show-field-errors gl-mt-3' } do |f|
= render 'new_group_fields', f: f, group_name_id: 'create-group-name'
#import-group-pane.tab-pane
diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml
index cdff533e3c7..a82f7803b44 100644
--- a/app/views/groups/settings/_advanced.html.haml
+++ b/app/views/groups/settings/_advanced.html.haml
@@ -1,3 +1,4 @@
+- remove_form_id = 'js-remove-group-form'
= render 'groups/settings/export', group: @group
.sub-section
@@ -26,6 +27,6 @@
= f.submit s_('GroupSettings|Change group URL'), class: 'btn gl-button btn-warning'
= render 'groups/settings/transfer', group: @group
-= render 'groups/settings/remove', group: @group
+= render 'groups/settings/remove', group: @group, remove_form_id: remove_form_id
= render_if_exists 'groups/settings/restore', group: @group
-= render_if_exists 'groups/settings/immediately_remove', group: @group
+= render_if_exists 'groups/settings/immediately_remove', group: @group, remove_form_id: remove_form_id
diff --git a/app/views/groups/settings/_membership.html.haml b/app/views/groups/settings/_membership.html.haml
index b05a294e864..c1566dddc00 100644
--- a/app/views/groups/settings/_membership.html.haml
+++ b/app/views/groups/settings/_membership.html.haml
@@ -4,3 +4,4 @@
= render 'shared/allow_request_access', form: f
= render_if_exists 'groups/member_lock_setting', f: f, group: @group
+= render_if_exists 'groups/user_caps_setting', f: f, group: @group
diff --git a/app/views/groups/settings/_permanent_deletion.html.haml b/app/views/groups/settings/_permanent_deletion.html.haml
index 125a20060ed..152cdfc1411 100644
--- a/app/views/groups/settings/_permanent_deletion.html.haml
+++ b/app/views/groups/settings/_permanent_deletion.html.haml
@@ -1,9 +1,11 @@
+- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
+
.sub-section
%h4.danger-title= _('Remove group')
- = form_tag(group, method: :delete) do
+ = form_tag(group, method: :delete, id: remove_form_id) do
%p
= _('Removing this group also removes all child projects, including archived projects, and their resources.')
%br
%strong= _('Removed group can not be restored!')
- = render 'groups/settings/remove_button', group: group
+ = render 'groups/settings/remove_button', group: group, remove_form_id: remove_form_id
diff --git a/app/views/groups/settings/_remove.html.haml b/app/views/groups/settings/_remove.html.haml
index a617467019a..8571b93364b 100644
--- a/app/views/groups/settings/_remove.html.haml
+++ b/app/views/groups/settings/_remove.html.haml
@@ -1,5 +1,6 @@
+- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
+
- if group.adjourned_deletion?
- = render_if_exists 'groups/settings/adjourned_deletion', group: group
+ = render_if_exists 'groups/settings/adjourned_deletion', group: group, remove_form_id: remove_form_id
- else
- = render 'groups/settings/permanent_deletion', group: group
-
+ = render 'groups/settings/permanent_deletion', group: group, remove_form_id: remove_form_id
diff --git a/app/views/groups/settings/_remove_button.html.haml b/app/views/groups/settings/_remove_button.html.haml
index a04dba68b92..1d5b7160049 100644
--- a/app/views/groups/settings/_remove_button.html.haml
+++ b/app/views/groups/settings/_remove_button.html.haml
@@ -1,7 +1,9 @@
+- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
+
- if group.paid?
.gl-alert.gl-alert-info.gl-mb-5{ data: { testid: 'group-has-linked-subscription-alert' } }
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
= html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
-= button_to _('Remove group'), '#', class: ['btn gl-button btn-danger js-confirm-danger', ('disabled' if group.paid?)], data: { 'confirm-danger-message' => remove_group_message(group), 'testid' => 'remove-group-button' }
+.js-confirm-danger{ data: group_settings_confirm_modal_data(group, remove_form_id) }
diff --git a/app/views/groups/settings/_transfer.html.haml b/app/views/groups/settings/_transfer.html.haml
index 1472ae42152..b2379d77314 100644
--- a/app/views/groups/settings/_transfer.html.haml
+++ b/app/views/groups/settings/_transfer.html.haml
@@ -14,7 +14,7 @@
%li= s_("GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.")
- if group.paid?
- .gl-alert.gl-alert-info.gl-mb-5{ data: { testid: 'group-to-transfer-has-linked-subscription-alert' } }
+ .gl-alert.gl-alert-info.gl-mb-5
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
= html_escape(_("This group can't be transfered because it is linked to a subscription. To transfer this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
diff --git a/app/views/groups/settings/applications/show.html.haml b/app/views/groups/settings/applications/show.html.haml
index fce3602349b..6e7f6ce4df0 100644
--- a/app/views/groups/settings/applications/show.html.haml
+++ b/app/views/groups/settings/applications/show.html.haml
@@ -6,4 +6,7 @@
%h3.page-title
= _("Group application: %{name}") % { name: @application.name }
-= render 'shared/doorkeeper/applications/show', edit_path: edit_group_settings_application_path(@group, @application), delete_path: group_settings_application_path(@group, @application)
+= render 'shared/doorkeeper/applications/show',
+ edit_path: edit_group_settings_application_path(@group, @application),
+ delete_path: group_settings_application_path(@group, @application),
+ index_path: group_settings_applications_path
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 2e74d983397..ed3f2b0c6db 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -32,8 +32,6 @@
= render_if_exists 'groups/group_activity_analytics', group: @group
-= render 'groups/invite_members_modal', group: @group
-
.groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
.top-area.group-nav-container.justify-content-between
.scrolling-tabs-container.inner-page-scroll-tabs
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 95888963947..f81afd0a82e 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -46,5 +46,5 @@
%button.btn-blank.btn-link.js-trigger-shortcut{ type: 'button' }
= _('Use shortcuts')
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
- %li= link_to _('Get a support subscription'), 'https://about.gitlab.com/pricing/'
- %li= link_to _('Compare GitLab editions'), 'https://about.gitlab.com/features/#compare'
+ %li= link_to _('Get a support subscription'), "https://#{ApplicationHelper.promo_host}/pricing/"
+ %li= link_to _('Compare GitLab editions'), "https://#{ApplicationHelper.promo_host}/features/#compare"
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index 3f7f929f766..ef6479f8be2 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -23,7 +23,7 @@
= form_tag personal_access_token_import_github_path, method: :post do
.form-group
%label.label-bold= _('Personal Access Token')
- = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' }, data: { qa_selector: 'personal_access_token_field' }
+ = text_field_tag :personal_access_token, '', class: 'form-control gl-form-input', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' }, data: { qa_selector: 'personal_access_token_field' }
%span.form-text.text-muted
= import_github_personal_access_token_message
diff --git a/app/views/issues/_issues_calendar.ics.ruby b/app/views/issues/_issues_calendar.ics.ruby
index c21c4dac9f0..4b0be661aa5 100644
--- a/app/views/issues/_issues_calendar.ics.ruby
+++ b/app/views/issues/_issues_calendar.ics.ruby
@@ -1,3 +1,4 @@
+# rubocop:disable Naming/FileName
# frozen_string_literal: true
cal = Icalendar::Calendar.new
@@ -17,3 +18,5 @@ end
# rubocop: enable CodeReuse/ActiveRecord
cal.to_ical
+
+# rubocop:enable Naming/FileName
diff --git a/app/views/jira_connect/branches/new.html.haml b/app/views/jira_connect/branches/new.html.haml
index f0e34c30018..74d547e6bb8 100644
--- a/app/views/jira_connect/branches/new.html.haml
+++ b/app/views/jira_connect/branches/new.html.haml
@@ -1,5 +1,6 @@
- @hide_breadcrumbs = true
- @hide_top_links = true
+- @content_class = 'limit-container-width'
- page_title _('New branch')
.js-jira-connect-create-branch{ data: @new_branch_data }
diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml
index cbe9a860210..be2be7288f8 100644
--- a/app/views/jira_connect/subscriptions/index.html.haml
+++ b/app/views/jira_connect/subscriptions/index.html.haml
@@ -9,20 +9,9 @@
= link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in'
%main.jira-connect-app.gl-px-5.gl-pt-7.gl-mx-auto
- - if current_user.blank? && @subscriptions.empty?
- .jira-connect-app-body.gl-px-5.gl-text-center
- %h2= s_('JiraService|GitLab for Jira Configuration')
- %p= s_('JiraService|Sign in to GitLab.com to get started.')
+ .js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) }
- .gl-mt-7
- = external_link _('Sign in to GitLab'), jira_connect_users_path, class: "btn gl-button btn-confirm js-jira-connect-sign-in"
-
- .gl-mt-7
- %p= s_('Integrations|Note: this integration only works with accounts on GitLab.com (SaaS).')
- - else
- .js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) }
-
- %p.jira-connect-app-body.gl-px-5.gl-mt-7.gl-font-base.gl-text-center
+ %p.jira-connect-app-body.gl-px-5.gl-font-base.gl-text-center.gl-mx-auto
%strong= s_('Integrations|Browser limitations')
- browser_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'
- firefox_link_start = browser_link_start.html_safe % { url: 'https://www.mozilla.org/en-US/firefox/' }
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index a89c621a55c..5ca4a2f9888 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -92,3 +92,5 @@
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/matomo' if extra_config.has_key?('matomo_url') && extra_config.has_key?('matomo_site_id')
= render 'layouts/snowplow'
+ -# This is needed by [GitLab JH](https://gitlab.com/gitlab-jh/gitlab/-/issues/184)
+ = render_if_exists "layouts/frontend_monitor"
diff --git a/app/views/layouts/_loading_hints.html.haml b/app/views/layouts/_loading_hints.html.haml
index e2189009045..9b2815ea9bc 100644
--- a/app/views/layouts/_loading_hints.html.haml
+++ b/app/views/layouts/_loading_hints.html.haml
@@ -1,4 +1,4 @@
-= cache_if(Feature.enabled?(:cached_loading_hints, current_user), [ActionController::Base.asset_host, user_application_theme, user_color_scheme], expires_in: 1.minute) do
+= cache([ActionController::Base.asset_host, user_application_theme, user_color_scheme], expires_in: 1.minute) do
- css_crossorigin = ActionController::Base.asset_host ? 'anonymous' : nil
- if ActionController::Base.asset_host
@@ -11,5 +11,5 @@
= preload_link_tag(path_to_stylesheet('application_utilities'), crossorigin: css_crossorigin)
= preload_link_tag(path_to_stylesheet('application'), crossorigin: css_crossorigin)
= preload_link_tag(path_to_stylesheet("highlight/themes/#{user_color_scheme}"), crossorigin: css_crossorigin)
- - if Gitlab::Tracking.enabled? && Gitlab::CurrentSettings.snowplow_collector_hostname
- %link{ rel: 'preconnect', href: Gitlab::CurrentSettings.snowplow_collector_hostname, crossorigin: '' }
+ - if Gitlab::Tracking.enabled? && Gitlab::Tracking.collector_hostname
+ %link{ rel: 'preconnect', href: Gitlab::Tracking.collector_hostname, crossorigin: '' }
diff --git a/app/views/layouts/_published_experiments.html.haml b/app/views/layouts/_published_experiments.html.haml
new file mode 100644
index 00000000000..717e1e633d2
--- /dev/null
+++ b/app/views/layouts/_published_experiments.html.haml
@@ -0,0 +1,4 @@
+= javascript_tag(nonce: content_security_policy_nonce) do
+ :plain
+ gl = window.gl || {};
+ gl.experiments = #{raw ApplicationExperiment.published_experiments.reject { |name, data| data[:excluded] }.to_json};
diff --git a/app/views/layouts/_snowplow.html.haml b/app/views/layouts/_snowplow.html.haml
index fc3b12acc46..7e242fb4a8e 100644
--- a/app/views/layouts/_snowplow.html.haml
+++ b/app/views/layouts/_snowplow.html.haml
@@ -10,5 +10,6 @@
window.snowplowOptions = #{Gitlab::Tracking.options(@group).to_json}
gl = window.gl || {};
- gl.snowplowStandardContext = #{Gitlab::Tracking::StandardContext.new.to_context.to_json.to_json}
+ gl.snowplowStandardContext = #{Gitlab::Tracking::StandardContext.new(namespace: @group || @project&.namespace,
+ project: @project, user: current_user).to_context.to_json.to_json}
gl.snowplowPseudonymizedPageUrl = #{masked_page_url.to_json};
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 899bf65de48..26e3d9b3b92 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -16,4 +16,5 @@
= render 'layouts/img_loader'
+ = render 'layouts/published_experiments'
= yield :scripts_body
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 8d28823bfa4..83e8ff79aec 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -120,7 +120,7 @@
- sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in')
= link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'gl-button btn btn-default btn-sign-in'
- %button.navbar-toggler.d-block.d-sm-none{ type: 'button', class: 'gl-border-none!', data: { testid: 'top-nav-responsive-toggle' } }
+ %button.navbar-toggler.d-block.d-sm-none{ type: 'button', class: 'gl-border-none!', data: { testid: 'top-nav-responsive-toggle', qa_selector: 'mobile_navbar_button' } }
%span.sr-only= _('Toggle navigation')
%span.more-icon.gl-px-3.gl-font-sm.gl-font-weight-bold
%span.gl-pr-2= _('Menu')
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index 01e59b8e2ef..a03cd4cd68f 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -10,7 +10,7 @@
%li
%button.js-shortcuts-modal-trigger{ type: "button" }
= _("Keyboard shortcuts")
- %span.text-secondary.float-right{ "aria-hidden": "true" }= '?'.html_safe
+ %kbd.flat.float-right{ "aria-hidden": "true" }= '?'.html_safe
%li.divider
%li
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
diff --git a/app/views/layouts/header/_logo_with_title.html.haml b/app/views/layouts/header/_logo_with_title.html.haml
index 9b5a47306d2..0b9d4e2eea4 100644
--- a/app/views/layouts/header/_logo_with_title.html.haml
+++ b/app/views/layouts/header/_logo_with_title.html.haml
@@ -1,4 +1,4 @@
%header.navbar.fixed-top.navbar-gitlab.justify-content-center
- = render 'shared/logo.svg'
+ = render partial: 'shared/logo', formats: :svg
%span.logo-text.d-none.d-lg-block.gl-ml-3.pt-1
- = render 'shared/logo_type.svg'
+ = render partial: 'shared/logo_type', formats: :svg
diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml
index 02a37dac158..3c52c430868 100644
--- a/app/views/layouts/nav/_breadcrumbs.html.haml
+++ b/app/views/layouts/nav/_breadcrumbs.html.haml
@@ -6,7 +6,7 @@
%nav.breadcrumbs{ class: [container, @content_class], 'aria-label': _('Breadcrumbs') }
.breadcrumbs-container{ class: ("border-bottom-0" if @no_breadcrumb_border) }
- if defined?(@left_sidebar)
- = button_tag class: 'toggle-mobile-nav', type: 'button' do
+ = button_tag class: 'toggle-mobile-nav', data: { qa_selector: 'toggle_mobile_nav_button' }, type: 'button' do
%span.sr-only= _("Open sidebar")
= sprite_icon('hamburger', size: 18)
.breadcrumbs-links{ data: { testid: 'breadcrumb-links', qa_selector: 'breadcrumb_links_content' } }
@@ -16,7 +16,7 @@
- if @breadcrumbs_extra_links
- @breadcrumbs_extra_links.each do |extra|
= breadcrumb_list_item link_to(extra[:text], extra[:link])
- = render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after
+ = render "layouts/nav/breadcrumbs/collapsed_inline_list", location: :after
- unless @skip_current_level_breadcrumb
%li
%h2.breadcrumbs-sub-title{ data: { qa_selector: 'breadcrumb_sub_title_content' } }
diff --git a/app/views/layouts/nav/_top_nav_responsive.html.haml b/app/views/layouts/nav/_top_nav_responsive.html.haml
index 86fd8b6d80c..22a260b5c0c 100644
--- a/app/views/layouts/nav/_top_nav_responsive.html.haml
+++ b/app/views/layouts/nav/_top_nav_responsive.html.haml
@@ -2,4 +2,5 @@
- view_model = top_nav_responsive_view_model(project: @project, group: @group)
.top-nav-responsive{ class: top_class }
- #js-top-nav-responsive{ data: { view_model: view_model.to_json } }
+ .cloak-startup
+ #js-top-nav-responsive{ data: { view_model: view_model.to_json } }
diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
deleted file mode 100644
index 8ea75087fed..00000000000
--- a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- dropdown_location = local_assigns.fetch(:location, nil)
-- button_tooltip = local_assigns.fetch(:title, _("Show parent pages"))
-- if defined?(@breadcrumb_dropdown_links) && @breadcrumb_dropdown_links.key?(dropdown_location)
- %li.dropdown
- %button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip }
- = sprite_icon("ellipsis_h", size: 12)
- = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
- .dropdown-menu
- %ul
- - @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index|
- %li{ style: "text-indent: #{[index * 16, 60].min}px;" }= link
diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml
new file mode 100644
index 00000000000..ccb660c050e
--- /dev/null
+++ b/app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml
@@ -0,0 +1,11 @@
+- dropdown_location = local_assigns.fetch(:location, nil)
+- button_tooltip = local_assigns.fetch(:title, _("Show all breadcrumbs"))
+- if defined?(@breadcrumb_collapsed_links) && @breadcrumb_collapsed_links.key?(dropdown_location)
+ %li.expander
+ %button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { container: "body" }, "aria-label": button_tooltip, title: button_tooltip }
+ = sprite_icon("ellipsis_h", size: 12)
+ = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
+ - @breadcrumb_collapsed_links[dropdown_location].each_with_index do |link, index|
+ %li{ :class => "gl-display-none! breadcrumbs-detail-item" }
+ = link
+ = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 842fb23d24a..f820f911d61 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -66,9 +66,9 @@
= _('Analytics')
%li.divider.fly-out-top-item
= nav_link(controller: :dev_ops_report) do
- = link_to admin_dev_ops_report_path, title: _('DevOps Report') do
+ = link_to admin_dev_ops_report_path, title: _('DevOps Reports') do
%span
- = _('DevOps Report')
+ = _('DevOps Reports')
= nav_link(controller: :usage_trends) do
= link_to admin_usage_trends_path, title: _('Usage Trends') do
%span
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index 4d5c354388f..caa46b7bc56 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -1,21 +1,22 @@
!!! 5
+- add_page_specific_style 'page_bundles/terms'
- @hide_breadcrumbs = true
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
%body{ data: { page: body_data_page } }
.layout-page.terms{ class: page_class }
- .content-wrapper
+ .content-wrapper.gl-pb-5
.mobile-overlay
.alert-wrapper
= render "layouts/broadcast"
= render 'layouts/header/read_only_banner'
- = render "layouts/flash", extra_flash_class: 'limit-container-width'
+ = render "layouts/flash"
%div{ class: "#{container_class} limit-container-width" }
.content{ id: "content-body" }
- .card
- .card-header
+ .gl-card
+ .gl-card-header
= brand_header_logo
- logo_text = brand_header_logo_type
- if logo_text.present?
diff --git a/app/views/notify/in_product_marketing_email.html.haml b/app/views/notify/in_product_marketing_email.html.haml
index 6382718480f..a85fa7c519f 100644
--- a/app/views/notify/in_product_marketing_email.html.haml
+++ b/app/views/notify/in_product_marketing_email.html.haml
@@ -166,10 +166,11 @@
= about_link('mailers/in_product_marketing/gitlab-logo-gray-rgb.png', 200)
%tr
%td{ "aria-hidden" => "true", height: "30", style: "font-size: 0; line-height: 0;" }
- %tr{ style: "background-color: #ffffff;" }
- %td{ style: "color: #424242; padding: 10px 30px; text-align: center; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;font-size: 16px; line-height: 22px; border: 1px solid #dddddd" }
- %p
- = @message.progress.html_safe
+ - if @message.series?
+ %tr{ style: "background-color: #ffffff;" }
+ %td{ style: "color: #424242; padding: 10px 30px; text-align: center; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;font-size: 16px; line-height: 22px; border: 1px solid #dddddd" }
+ %p
+ = @message.progress.html_safe
%tr
%td{ bgcolor: "#ffffff", height: "auto", style: "max-width: 600px; width: 100%; text-align: center; height: 200px; padding: 25px 15px; mso-line-height-rule: exactly; min-height: 40px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;", valign: "middle", width: "100%" }
= inline_image_link(@message.logo_path, { width: '150', style: 'width: 150px;' })
@@ -210,6 +211,12 @@
%td{ style: "padding: 10px 20px 30px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#000000; font-size: 18px; line-height: 24px;" }
%p{ style: "margin: 0 0 50px 0;" }
= @message.feedback_thanks
+ - if @message.invite_members?
+ %tr
+ %td{ align: "center", style: "padding: 0 20px 80px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
+ = @message.invite_text
+ %br
+ = @message.invite_link
%tr{ style: "background-color: #ffffff;" }
%td{ align: "center", style: "padding:75px 20px 25px;" }
= about_link('gitlab_logo.png', 80)
diff --git a/app/views/notify/in_product_marketing_email.text.erb b/app/views/notify/in_product_marketing_email.text.erb
index 6f0a2efa410..79a366eb1cc 100644
--- a/app/views/notify/in_product_marketing_email.text.erb
+++ b/app/views/notify/in_product_marketing_email.text.erb
@@ -21,6 +21,10 @@
<%= @message.feedback_thanks %>
<% end %>
+<% if @message.invite_members? %>
+<%= @message.invite_text %>
+<%= @message.invite_link %>
+<% end %>
diff --git a/app/views/notify/member_access_denied_email.html.haml b/app/views/notify/member_access_denied_email.html.haml
index 11661a423dd..eeef66d353d 100644
--- a/app/views/notify/member_access_denied_email.html.haml
+++ b/app/views/notify/member_access_denied_email.html.haml
@@ -2,6 +2,11 @@
%td.text-content
%p
Your request to join the
- #{link_to member_source.human_name, member_source.web_url, class: :highlight} #{member_source.model_name.singular}
- has been #{content_tag :span, 'denied', class: :highlight}.
+
+ - if @source_hidden
+ #{content_tag :span, 'Hidden', class: :highlight}
+ - else
+ #{link_to member_source.human_name, member_source.web_url, class: :highlight}
+
+ #{member_source.model_name.singular} has been #{content_tag :span, 'denied', class: :highlight}.
diff --git a/app/views/notify/member_access_granted_email.html.haml b/app/views/notify/member_access_granted_email.html.haml
index e28a10a243f..cce36f7b8a6 100644
--- a/app/views/notify/member_access_granted_email.html.haml
+++ b/app/views/notify/member_access_granted_email.html.haml
@@ -8,7 +8,11 @@
%td.text-content
%p
= _('You have been granted %{access_level} access to the %{source_link} %{source_type}.').html_safe % { access_level: access_level, source_link: source_link, source_type: source_type }
+ - if member.tasks_to_be_done.present?
+ = s_("InviteEmail|You were assigned the following tasks:")
+ %ul.list-style-position-inside
+ - member.tasks_to_be_done.each do |task|
+ %li= localized_tasks_to_be_done_choices[task]
%p
- leave_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: leave_link }
= _('If this was a mistake you can %{leave_link_start}leave the %{source_type}%{link_end}.').html_safe % { source_type: source_type, leave_link_start: leave_link_start, link_end: link_end }
-
diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml
index 5489a2ac6a1..1d1f696e1b2 100644
--- a/app/views/notify/member_invited_email.html.haml
+++ b/app/views/notify/member_invited_email.html.haml
@@ -24,6 +24,11 @@
%p
- if member.created_by
= html_escape(s_("InviteEmail|%{inviter} invited you to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders.merge({ inviter: (link_to inviter_name, user_url(member.created_by)).html_safe })
+ - if member.tasks_to_be_done.present?
+ = s_("InviteEmail|and has assigned you the following tasks:")
+ %ul.list-style-position-inside
+ - member.tasks_to_be_done.each do |task|
+ %li= localized_tasks_to_be_done_choices[task]
- else
= html_escape(s_("InviteEmail|You are invited to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders
%p.invite-actions
diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml
index f74902a3c3b..67c649a9fce 100644
--- a/app/views/profiles/_event_table.html.haml
+++ b/app/views/profiles/_event_table.html.haml
@@ -3,10 +3,11 @@
%ul.content-list
- events.each do |event|
- %li
- %span.description
- = audit_icon(event.details[:with], css_class: 'gl-mr-2')
- = _('Signed in with %{authentication} authentication') % { authentication: event.details[:with]}
- %span.float-right= time_ago_with_tooltip(event.created_at)
+ - if event.success?
+ %li
+ %span.description
+ = audit_icon('key', css_class: 'gl-mr-2')
+ = _('Signed in with %{authentication} authentication') % { authentication: event.provider }
+ %span.float-right= time_ago_with_tooltip(event.created_at)
= paginate events, theme: "gitlab"
diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml
index aec855c790e..4bbb4a21b39 100644
--- a/app/views/profiles/audit_log.html.haml
+++ b/app/views/profiles/audit_log.html.haml
@@ -6,6 +6,6 @@
%h4.gl-mt-0
= page_title
%p
- = _('This is a security log of important events involving your account.')
+ = _('This is a security log of authentication events involving your account.')
.col-lg-8
= render 'event_table', events: @events
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 35bdfbb1c29..0cfe65994da 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -44,7 +44,7 @@
%span.badge.badge-muted.badge-pill.gl-badge.badge-info= s_('Profiles|Public email')
- if @primary_email === current_user.notification_email_or_default
%span.badge.badge-muted.badge-pill.gl-badge.badge-info= s_('Profiles|Default notification email')
- - @emails.each do |email|
+ - @emails.reject(&:user_primary_email?).each do |email|
%li{ data: { qa_selector: 'email_row_content' } }
= render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
%span.float-right
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 3e41f107e04..b1470520eea 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -71,7 +71,7 @@
placeholder: s_("Profiles|What's your status?")
.checkbox-icon-inline-wrapper
= status_form.check_box :availability, { data: { testid: "user-availability-checkbox" }, label: s_("Profiles|Busy"), wrapper_class: 'gl-mr-0 gl-font-weight-bold' }, availability["busy"], availability["not_set"]
- .gl-text-gray-600.gl-ml-5= s_('Profiles|"Busy" will be shown next to your name')
+ .gl-text-gray-600.gl-ml-5= s_('Profiles|An indicator appears next to your name and avatar')
.col-lg-12
%hr
.row.user-time-preferences.js-search-settings-section
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 00df8608957..0eae3c95bf6 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -43,7 +43,9 @@
.gl-alert.gl-alert-danger.gl-mb-5
.gl-alert-container
.gl-alert-content
- = @error
+ %p.gl-alert-body.gl-md-0
+ = @error[:message]
+ = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= label_tag :pin_code, _('Pin code'), class: "label-bold"
= text_field_tag :pin_code, nil, class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' }
diff --git a/app/views/projects/_invite_members_side_nav_link.html.haml b/app/views/projects/_invite_members_side_nav_link.html.haml
new file mode 100644
index 00000000000..ea6174d19f0
--- /dev/null
+++ b/app/views/projects/_invite_members_side_nav_link.html.haml
@@ -0,0 +1,8 @@
+.js-invite-members-trigger{ data: { trigger_source: 'project-side-nav',
+ classes: 'gl-text-decoration-none! gl-shadow-none! gl-text-body!',
+ icon: 'users',
+ display_text: title,
+ trigger_element: 'side-nav'} }
+
+= render partial: 'shared/nav/sidebar_submenu', locals: { sidebar_menu: sidebar_menu }
+= render 'projects/invite_members_modal', project: project
diff --git a/app/views/projects/_merge_request_merge_commit_template.html.haml b/app/views/projects/_merge_request_merge_commit_template.html.haml
new file mode 100644
index 00000000000..185b730e0bb
--- /dev/null
+++ b/app/views/projects/_merge_request_merge_commit_template.html.haml
@@ -0,0 +1,17 @@
+- form = local_assigns.fetch(:form)
+
+.form-group
+ %b= s_('ProjectSettings|Merge commit message template')
+ %p.text-secondary
+ - configure_the_merge_commit_message_help_link_url = help_page_path('user/project/merge_requests/commit_templates.md', anchor: 'merge-commit-message-template')
+ - configure_the_merge_commit_message_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: configure_the_merge_commit_message_help_link_url }
+ = s_('ProjectSettings|The commit message used when merging, if the merge method creates a merge commit. %{link_start}Learn more about syntax and variables.%{link_end}').html_safe % { link_start: configure_the_merge_commit_message_help_link_start, link_end: '</a>'.html_safe }
+ .mb-2
+ - default_merge_commit_template = "Merge branch '%{source_branch}' into '%{target_branch}'\n\n%{title}\n\n%{issues}\n\nSee merge request %{reference}"
+ = form.text_area :merge_commit_template, class: 'form-control gl-form-input', rows: 8, maxlength: 500, placeholder: default_merge_commit_template
+ %p.form-text.text-muted
+ = s_('ProjectSettings|Maximum 500 characters.')
+ = s_('ProjectSettings|Supported variables:')
+ - Gitlab::MergeRequests::MergeCommitMessage::PLACEHOLDERS.keys.each do |placeholder|
+ %code
+ = "%{#{placeholder}}".html_safe
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index f595b4f709b..c5a25bec6eb 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -10,5 +10,7 @@
= render 'projects/merge_request_merge_suggestions_settings', project: @project, form: form
+= render 'projects/merge_request_merge_commit_template', project: @project, form: form
+
- if @project.forked?
= render 'projects/merge_request_target_project_settings', project: @project, form: form
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 256c3ebad0a..c21240b340c 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -40,12 +40,17 @@
.form-group.project-path.col-sm-6
= f.label :path, class: 'label-bold' do
%span= _("Project slug")
- = f.text_field :path, placeholder: "my-awesome-project", class: "form-control gl-form-input", required: true, aria: { required: true }
+ = f.text_field :path, placeholder: "my-awesome-project", class: "form-control gl-form-input", required: true, aria: { required: true }, data: { username: current_user.username }
- if current_user.can_create_group?
.form-text.text-muted
- link_start_group_path = '<a href="%{path}">' % { path: new_group_path }
- project_tip = s_('ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}') % { link_start: link_start_group_path, link_end: '</a>' }
= project_tip.html_safe
+.gl-alert.gl-alert-success.gl-mb-4.gl-display-none.js-user-readme-repo
+ = sprite_icon('check-circle', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-body
+ - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/profile/index', anchor: 'user-profile-readme') }
+ = html_escape(_('%{project_path} is a project that you can use to add a README to your GitLab profile. Create a public project and initialize the repository with a README to get started. %{help_link_start}Learn more.%{help_link_end}')) % { project_path: "<strong>#{current_user.username} / #{current_user.username}</strong>".html_safe, help_link_start: help_link_start, help_link_end: '</a>'.html_safe }
.form-group
= f.label :description, class: 'label-bold' do
@@ -72,7 +77,7 @@
- e.try do
.form-group
.form-check.gl-mb-3
- = check_box_tag 'project[initialize_with_sast]', '1', true, class: 'form-check-input', data: { track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
+ = check_box_tag 'project[initialize_with_sast]', '1', true, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
.form-text.text-muted
@@ -81,7 +86,7 @@
- e.try(:free_indicator) do
.form-group
.form-check.gl-mb-3
- = check_box_tag 'project[initialize_with_sast]', '1', true, class: 'form-check-input', data: { track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
+ = check_box_tag 'project[initialize_with_sast]', '1', true, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
%span.badge.badge-info.badge-pill.gl-badge.sm= _('Free')
diff --git a/app/views/projects/_remove.html.haml b/app/views/projects/_remove.html.haml
index cb0ec9f19c8..815e76ebcb9 100644
--- a/app/views/projects/_remove.html.haml
+++ b/app/views/projects/_remove.html.haml
@@ -1,4 +1,6 @@
- return unless can?(current_user, :remove_project, project)
+- merge_requests_count = Projects::AllMergeRequestsCountService.new(project).count
+- issues_count = Projects::AllIssuesCountService.new(project).count
.sub-section
%h4.danger-title= _('Delete project')
@@ -7,4 +9,4 @@
= link_to _('Learn more.'), help_page_path('user/project/settings/index', anchor: 'removing-a-fork-relationship'), target: '_blank', rel: 'noopener noreferrer'
%p
%strong= _('Deleted projects cannot be restored!')
- #js-project-delete-button{ data: { form_path: project_path(project), confirm_phrase: project.path } }
+ #js-project-delete-button{ data: { form_path: project_path(project), confirm_phrase: delete_confirm_phrase(project), is_fork: project.forked?.to_s, issues_count: number_with_delimiter(issues_count), merge_requests_count: number_with_delimiter(merge_requests_count), forks_count: number_with_delimiter(project.forks_count), stars_count: number_with_delimiter(project.star_count) } }
diff --git a/app/views/projects/_remove_fork.html.haml b/app/views/projects/_remove_fork.html.haml
index 8fa21966683..92eb29dc407 100644
--- a/app/views/projects/_remove_fork.html.haml
+++ b/app/views/projects/_remove_fork.html.haml
@@ -8,4 +8,4 @@
%p
%strong= _('Once removed, the fork relationship cannot be restored. This project will no longer be able to receive or send merge requests to the source project or other forks.')
= link_to _('Learn more.'), help_page_path('user/project/settings/index', anchor: 'removing-a-fork-relationship'), target: '_blank', rel: 'noopener noreferrer'
- = button_to _('Remove fork relationship'), '#', class: "gl-button btn btn-danger js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) }
+ = button_to _('Remove fork relationship'), '#', class: "gl-button btn btn-danger js-legacy-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) }
diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml
index 7b345941cf7..63cf4dfe0ab 100644
--- a/app/views/projects/_service_desk_settings.html.haml
+++ b/app/views/projects/_service_desk_settings.html.haml
@@ -14,8 +14,9 @@
custom_email: (@project.service_desk_custom_address if @project.service_desk_enabled),
custom_email_enabled: "#{Gitlab::ServiceDeskEmail.enabled?}",
selected_template: "#{@project.service_desk_setting&.issue_template_key}",
+ selected_file_template_project_id: "#{@project.service_desk_setting&.file_template_project_id}",
outgoing_name: "#{@project.service_desk_setting&.outgoing_name}",
project_key: "#{@project.service_desk_setting&.project_key}",
- templates: issuable_templates_names(Issue.new) } }
+ templates: available_service_desk_templates_for(@project) } }
- elsif show_callout?('promote_service_desk_dismissed')
= render 'shared/promotions/promote_servicedesk'
diff --git a/app/views/projects/_transfer.html.haml b/app/views/projects/_transfer.html.haml
index ee717c2deca..e48008e1cc6 100644
--- a/app/views/projects/_transfer.html.haml
+++ b/app/views/projects/_transfer.html.haml
@@ -14,4 +14,4 @@
= label_tag :new_namespace_id, _('Select a new namespace'), class: 'gl-font-weight-bold'
.form-group
= select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2'
- = f.submit 'Transfer project', class: "gl-button btn btn-danger js-confirm-danger qa-transfer-button", data: { "confirm-danger-message" => transfer_project_message(@project) }
+ = f.submit 'Transfer project', class: "gl-button btn btn-danger js-legacy-confirm-danger qa-transfer-button", data: { "confirm-danger-message" => transfer_project_message(@project) }
diff --git a/app/views/projects/_visibility_modal.html.haml b/app/views/projects/_visibility_modal.html.haml
index f75216a71b6..db98b978f04 100644
--- a/app/views/projects/_visibility_modal.html.haml
+++ b/app/views/projects/_visibility_modal.html.haml
@@ -20,10 +20,10 @@
%li
= _("Current forks will keep their visibility level.").html_safe
%label{ for: "confirm_path_input" }
- = _("To confirm, type %{phrase_code}").html_safe % { phrase_code: '<code class="js-confirm-danger-match">%{phrase_name}</code>'.html_safe % { phrase_name: @project.full_path } }
+ = _("To confirm, type %{phrase_code}").html_safe % { phrase_code: '<code class="js-legacy-confirm-danger-match">%{phrase_name}</code>'.html_safe % { phrase_name: @project.full_path } }
.form-group
- = text_field_tag 'confirm_path_input', '', class: 'form-control js-confirm-danger-input qa-confirm-input'
+ = text_field_tag 'confirm_path_input', '', class: 'form-control js-legacy-confirm-danger-input qa-confirm-input'
.form-actions
%button.btn.gl-button.btn-default.gl-mr-4{ type: "button", "data-dismiss": "modal" }
= _('Cancel')
- = submit_tag _('Reduce project visibility'), class: "btn gl-button btn-danger js-confirm-danger-submit qa-confirm-button", disabled: true
+ = submit_tag _('Reduce project visibility'), class: "btn gl-button btn-danger js-legacy-confirm-danger-submit qa-confirm-button", disabled: true
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 30c052e054f..704576619a7 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -20,7 +20,7 @@
%span.legend-box.legend-box-9
%span.right-label Older
- .table-responsive.file-content.blame.code.js-syntax-highlight
+ .table-responsive.file-content.blame.code{ class: user_color_scheme }
%table
- current_line = 1
- @blame.groups.each do |blame_group|
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index d7668dd1c91..dad4ea205b4 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -10,7 +10,7 @@
= edit_blob_button(@project, @ref, @path, blob: blob)
= ide_edit_button(@project, @ref, @path, blob: blob)
- if can_view_pipeline_editor?(@project) && @path == @project.ci_config_path_or_default
- = link_to "Pipeline Editor", project_ci_pipeline_editor_path(@project), class: "btn gl-button btn-confirm-secondary gl-ml-3"
+ = link_to "Pipeline Editor", project_ci_pipeline_editor_path(@project, branch_name: @ref), class: "btn gl-button btn-confirm-secondary gl-ml-3"
.btn-group{ role: "group", class: ("gl-ml-3" if current_user) }>
= render_if_exists 'projects/blob/header_file_locks_link'
- if current_user
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index ecbef9a11a7..8378ce2c7e5 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -16,14 +16,10 @@
%h3.page-title.blob-edit-page-title
Edit file
.file-editor
- %ul.nav-links.no-bottom.js-edit-mode.nav.nav-tabs
- %li.active
- = link_to '#editor' do
- Write
+ = gl_tabs_nav({ class: 'js-edit-mode nav-links gl-border-0'}) do
+ = gl_tab_link_to _('Write'), '#editor', { tab_class: 'active' }
- %li
- = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id) do
- = editing_preview_title(@blob.name)
+ = gl_tab_link_to editing_preview_title(@blob.name), '#preview', { data: { 'preview-url': project_preview_blob_path(@project, @id) } }
= form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths(@project)) do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
diff --git a/app/views/projects/blob/viewers/_changelog.html.haml b/app/views/projects/blob/viewers/_changelog.html.haml
index cac858c1444..5f3b9285ea9 100644
--- a/app/views/projects/blob/viewers/_changelog.html.haml
+++ b/app/views/projects/blob/viewers/_changelog.html.haml
@@ -1,3 +1,3 @@
= sprite_icon('history', css_class: 'gl-mr-1 gl-vertical-align-text-bottom')
= succeed '.' do
- = _("To find the state of this project's repository at the time of any of these versions, check out %{link_start}the tags%{link_end}.").html_safe % { link_start: "<a href='#{project_tags_path(viewer.project)}'>".html_safe, link_end: "</a>".html_safe }
+ = _("To find the state of this project's repository at the time of any of these versions, check out %{link_start}the tags%{link_end}").html_safe % { link_start: "<a href='#{project_tags_path(viewer.project)}'>".html_safe, link_end: "</a>".html_safe }
diff --git a/app/views/projects/blob/viewers/_readme.html.haml b/app/views/projects/blob/viewers/_readme.html.haml
index e06ff4edf71..c07428f0337 100644
--- a/app/views/projects/blob/viewers/_readme.html.haml
+++ b/app/views/projects/blob/viewers/_readme.html.haml
@@ -1,4 +1,4 @@
= sprite_icon('information-o', css_class: 'gl-vertical-align-middle! gl-mr-2')
= succeed '.' do
- link_to_wiki = link_to(_("the wiki"), wiki_path(viewer.project.wiki))
- = _("To learn more about this project, read %{link_to_wiki}.").html_safe % { link_to_wiki: link_to_wiki }
+ = _("To learn more about this project, read %{link_to_wiki}").html_safe % { link_to_wiki: link_to_wiki }
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 99a9535b8e8..be6efa310b9 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -23,7 +23,7 @@
- if commit
= render 'projects/branches/commit', commit: commit, project: @project
- else
- = s_('Branches|Cant find HEAD commit for this branch')
+ = s_('Branches|Can’t find HEAD commit for this branch')
- if branch.name != @repository.root_ref
.js-branch-divergence-graph
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index 41f5fb3dcbd..60a79456c36 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -1,12 +1,10 @@
- any_pipelines = @commit.present(current_user: current_user).any_pipelines?
-%ul.nav-links.no-top.no-bottom.commit-ci-menu.nav.nav-tabs
- = nav_link(path: 'commit#show') do
- = link_to project_commit_path(@project, @commit.id) do
- = _('Changes')
- %span.badge.badge-pill= @diffs.size
+= gl_tabs_nav({ class: 'commit-ci-menu gl-display-flex gl-flex-grow-1'}) do
+ = gl_tab_link_to project_commit_path(@project, @commit.id) do
+ = _('Changes')
+ = gl_tab_counter_badge(@diffs.size)
- if any_pipelines
- = nav_link(path: 'commit#pipelines') do
- = link_to pipelines_project_commit_path(@project, @commit.id) do
- = _('Pipelines')
- %span.badge.badge-pill.js-pipelines-mr-count= @commit.pipelines.size
+ = gl_tab_link_to pipelines_project_commit_path(@project, @commit.id) do
+ = _('Pipelines')
+ = gl_tab_counter_badge(@commit.pipelines.size, { class: 'js-pipelines-mr-count' })
diff --git a/app/views/projects/commit/_multiple_signatures_signature_badge.html.haml b/app/views/projects/commit/_multiple_signatures_signature_badge.html.haml
new file mode 100644
index 00000000000..74515438af2
--- /dev/null
+++ b/app/views/projects/commit/_multiple_signatures_signature_badge.html.haml
@@ -0,0 +1,6 @@
+- title = capture do
+ = html_escape(_('This commit was signed with %{strong_open}multiple%{strong_close} signatures.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
+
+- locals = { signature: signature, title: title, label: _('Unverified'), css_class: 'invalid', icon: 'status_notfound_borderless' }
+
+= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/confluences/show.html.haml b/app/views/projects/confluences/show.html.haml
index cf4e39f9659..413de90b67b 100644
--- a/app/views/projects/confluences/show.html.haml
+++ b/app/views/projects/confluences/show.html.haml
@@ -6,9 +6,8 @@
= s_('WikiEmpty|Confluence is enabled')
%p
- wiki_confluence_epic_link_url = 'https://gitlab.com/groups/gitlab-org/-/epics/3629'
- - wiki_confluence_epic_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe, url: wiki_confluence_epic_link_url)
- = format(s_("WikiEmpty|You've enabled the Confluence Workspace integration. Your wiki will be viewable directly within Confluence. We are hard at work integrating Confluence more seamlessly into GitLab. If you'd like to stay up to date, follow our %{wiki_confluence_epic_link_start}Confluence epic%{wiki_confluence_epic_link_end}.").html_safe, wiki_confluence_epic_link_start: wiki_confluence_epic_link_start, wiki_confluence_epic_link_end: '</a>'.html_safe)
+ - wiki_confluence_epic_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wiki_confluence_epic_link_url }
+ = html_escape(s_("WikiEmpty|You've enabled the Confluence Workspace integration. Your wiki will be viewable directly within Confluence. We are hard at work integrating Confluence more seamlessly into GitLab. If you'd like to stay up to date, follow our %{wiki_confluence_epic_link_start}Confluence epic%{wiki_confluence_epic_link_end}.")) % { wiki_confluence_epic_link_start: wiki_confluence_epic_link_start, wiki_confluence_epic_link_end: '</a>'.html_safe }
= link_to @project.confluence_integration.confluence_url, target: '_blank', rel: 'noopener noreferrer', class: 'gl-button btn btn-success external-url', title: s_('WikiEmpty|Go to Confluence') do
- = sprite_icon('external-link')
= s_('WikiEmpty|Go to Confluence')
-
+ = sprite_icon('external-link')
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index f398ac6ede7..ba79f0ee3cb 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,4 +1,6 @@
- page_title _("Value Stream Analytics")
+- data_attributes = @request_params.valid? ? @request_params.to_data_attributes : {}
+- data_attributes.merge!(cycle_analytics_initial_data(@project, @group))
- add_page_specific_style 'page_bundles/cycle_analytics'
-#js-cycle-analytics{ data: cycle_analytics_initial_data(@project, @group) }
+#js-cycle-analytics{ data: data_attributes }
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index 57dfcb8cf4a..f18574c3ad5 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -6,7 +6,7 @@
.table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("ID")
- %strong.table-mobile-content ##{deployment.iid}
+ %strong.table-mobile-content{ data: { testid: 'deployment-id' } } ##{deployment.iid}
.table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Triggerer")
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 926a0610577..6421aef14cf 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -23,7 +23,7 @@
.js-project-permissions-form
- if show_visibility_confirm_modal?(@project)
= render "visibility_modal"
- = f.submit _('Save changes'), class: "btn gl-button btn-confirm #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
+ = f.submit _('Save changes'), class: "btn gl-button btn-confirm #{('js-legacy-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
.settings-header
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 905794c0730..c3fbf774faa 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -77,5 +77,5 @@
git push -u origin --all
git push -u origin --tags
-- if @project.empty_repo_upload_experiment?
+- if @project.upload_anchor_data.present?
= render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, default_branch_name), ref: default_branch_name, method: :post
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 0136184f80d..72ccc8d830c 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -1,10 +1,19 @@
- page_title _("Environments")
-- add_page_specific_style 'page_bundles/environments'
-#environments-list-view{ data: { environments_data: environments_list_data,
- "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
- "can-create-environment" => can?(current_user, :create_environment, @project).to_s,
- "new-environment-path" => new_project_environment_path(@project),
- "help-page-path" => help_page_path("ci/environments/index.md"),
- "project-path" => @project.full_path,
- "default-branch-name" => @project.default_branch_or_main } }
+- if Feature.enabled?(:new_environments_table)
+ #environments-table{ data: { endpoint: project_environments_path(@project, format: :json),
+ "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
+ "can-create-environment" => can?(current_user, :create_environment, @project).to_s,
+ "new-environment-path" => new_project_environment_path(@project),
+ "help-page-path" => help_page_path("ci/environments/index.md"),
+ "project-path" => @project.full_path,
+ "default-branch-name" => @project.default_branch_or_main } }
+- else
+ - add_page_specific_style 'page_bundles/environments'
+ #environments-list-view{ data: { environments_data: environments_list_data,
+ "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
+ "can-create-environment" => can?(current_user, :create_environment, @project).to_s,
+ "new-environment-path" => new_project_environment_path(@project),
+ "help-page-path" => help_page_path("ci/environments/index.md"),
+ "project-path" => @project.full_path,
+ "default-branch-name" => @project.default_branch_or_main } }
diff --git a/app/views/projects/google_cloud/index.html.haml b/app/views/projects/google_cloud/index.html.haml
index 4fc66e17810..69e481501d5 100644
--- a/app/views/projects/google_cloud/index.html.haml
+++ b/app/views/projects/google_cloud/index.html.haml
@@ -3,81 +3,4 @@
- @content_class = "limit-container-width" unless fluid_layout
-#js-google-cloud
-
- %h1.gl-font-size-h1 Google Cloud
-
- %section#js-section-google-cloud-service-accounts
-
- %h2.gl-font-size-h2 Service Accounts
-
- %p= _('Service Accounts keys are required to authorize GitLab to deploy your Google Cloud project.')
-
- %table.table.b-table.gl-table
-
- %thead
- %tr
- %th Environment
- %th GCP Project ID
- %th Service Account Key
-
- %tbody
-
- %tr
- %td *
- %td serving-salutes-453
- %td .....
-
- %tr
- %td production
- %td crimson-corey-234
- %td .....
-
- %tr
- %td review/*
- %td roving-river-379
- %td .....
-
- %a.gl-button.btn.btn-primary= _('Add new service account')
-
- %br
-
- %section#js-section-google-cloud-deployments
-
- .row.row-fluid
-
- .col-lg-4
- %h2.gl-font-size-h2 Deployments
- %p= _('Google Cloud offers several deployment targets. Select the one most suitable for your project.')
- %p
- = _('Deployments to Google Kubernetes Engine can be ')
- %a{ href: '#' }= _('managed')
- = _('in Infrastructure :: Kubernetes clusters')
-
- .col-lg-8
-
- %br
-
- .gl-card.gl-mb-6
- .gl-card-body
- .gl-display-flex.gl-align-items-baseline
- %strong.gl-font-lg App Engine
- .gl-ml-auto.gl-text-gray-500 Disabled
- %p= _('App Engine description and apps that are suitable for this deployment target')
- %button.gl-button.btn.btn-default= _('Configure via Merge Request')
-
- .gl-card.gl-mb-6
- .gl-card-body
- .gl-display-flex.gl-align-items-baseline
- %strong.gl-font-lg Cloud Functions
- .gl-ml-auto.gl-text-gray-500 Disabled
- %p= _('Cloud Functions description and apps that are suitable for this deployment target')
- %button.gl-button.btn.btn-default= _('Configure via Merge Request')
-
- .gl-card.gl-mb-6
- .gl-card-body
- .gl-display-flex.gl-align-items-baseline
- %strong.gl-font-lg Cloud Run
- .gl-ml-auto.gl-text-gray-500 Disabled
- %p= _('Cloud Run description and apps that are suitable for this deployment target')
- %button.gl-button.btn.btn-default= _('Configure via Merge Request')
+#js-google-cloud{ data: @js_data }
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
index 86dfa1929d6..f6861e4119e 100644
--- a/app/views/projects/hook_logs/show.html.haml
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -2,11 +2,11 @@
- add_to_breadcrumbs _('Webhook Settings'), namespace_project_hooks_path
- page_title _('Webhook Logs')
-.row.gl-mt-3.gl-mb-3
- .col-lg-3
- %h4.gl-mt-0
- = _("Request details")
- .col-lg-9
- = link_to _('Resend Request'), @hook_log.present.retry_path, method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
-
- = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
+%h3.page-title
+ = _("Request details")
+
+%hr
+
+= link_to _("Resend Request"), @hook_log.present.retry_path, method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
+
+= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 2fbaa5812c0..f1c19756474 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,5 +1,4 @@
-- add_page_startup_api_call discussions_path(@issue)
-- add_page_startup_api_call notes_url
+- add_page_startup_api_call Feature.enabled?(:paginated_issue_discussions, @project, default_enabled: :yaml) ? discussions_path(@issue, per_page: 20) : discussions_path(@issue)
- @gfm_form = true
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index e4d072a9472..1ab51df6d24 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,5 +1,5 @@
- is_project_overview = local_assigns.fetch(:is_project_overview, false)
-= render 'shared/alerts/positioning_disabled'
+= render 'shared/alerts/positioning_disabled' if @sort == 'relative_position'
- if Feature.enabled?(:vue_issuables_list, @project) && !is_project_overview
- data_endpoint = local_assigns.fetch(:data_endpoint, expose_path(api_v4_projects_issues_path(id: @project.id)))
diff --git a/app/views/projects/issues/_service_desk_empty_state.html.haml b/app/views/projects/issues/_service_desk_empty_state.html.haml
index a4251c8e5dc..afeb2a23ea2 100644
--- a/app/views/projects/issues/_service_desk_empty_state.html.haml
+++ b/app/views/projects/issues/_service_desk_empty_state.html.haml
@@ -16,7 +16,7 @@
= s_("ServiceDesk|Your users can send emails to this address:")
%code= @project.service_desk_address
- %span= s_("ServiceDesk|Issues created from Service Desk emails appear here. Each comment becomes part of the email conversation.")
+ %span= s_("ServiceDesk|Issues created from Service Desk emails will appear here. Each comment becomes part of the email conversation.")
= link_to _('Learn more.'), help_page_path('user/project/service_desk')
- if can_edit_project_settings && !service_desk_enabled
diff --git a/app/views/projects/issues/_service_desk_info_content.html.haml b/app/views/projects/issues/_service_desk_info_content.html.haml
index 448a341eba6..f0ec68ba54b 100644
--- a/app/views/projects/issues/_service_desk_info_content.html.haml
+++ b/app/views/projects/issues/_service_desk_info_content.html.haml
@@ -1,16 +1,17 @@
- service_desk_enabled = @project.service_desk_enabled?
- can_edit_project_settings = can?(current_user, :admin_project, @project)
+- can_admin_issues = can?(current_user, :admin_issue, @project)
- title_text = s_("ServiceDesk|Use Service Desk to connect with your users and offer customer support through email right inside GitLab")
.non-empty-state.media
.svg-content
- = render 'shared/empty_states/icons/service_desk_callout.svg'
+ = render partial: 'shared/empty_states/icons/service_desk_callout', formats: :svg
.gl-mt-3.gl-ml-3
%h5= title_text
- - if can_edit_project_settings && service_desk_enabled
+ - if can_admin_issues && service_desk_enabled
%p
= s_("ServiceDesk|Your users can send emails to this address:")
%code= @project.service_desk_address
diff --git a/app/views/projects/learn_gitlab/index.html.haml b/app/views/projects/learn_gitlab/index.html.haml
index 4935b72d3fa..9b17be99da0 100644
--- a/app/views/projects/learn_gitlab/index.html.haml
+++ b/app/views/projects/learn_gitlab/index.html.haml
@@ -1,5 +1,12 @@
- breadcrumb_title _("Learn GitLab")
- page_title _("Learn GitLab")
- add_page_specific_style 'page_bundles/learn_gitlab'
+- data = learn_gitlab_data(@project)
+- invite_members_open = session.delete(:confetti_post_signup)
-#js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json, sections: onboarding_sections_data.to_json } }
+- experiment(:confetti_post_signup, actor: current_user) do |e|
+ - e.control do
+ #js-learn-gitlab-app{ data: data }
+ - e.candidate do
+ = render 'projects/invite_members_modal', project: @project
+ #js-learn-gitlab-app{ data: data.merge(invite_members_open: invite_members_open) }
diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml
index 459742c3b81..0c8af873095 100644
--- a/app/views/projects/merge_requests/_widget.html.haml
+++ b/app/views/projects/merge_requests/_widget.html.haml
@@ -14,8 +14,8 @@
window.gl.mrWidgetData.pipeline_must_succeed_docs_path = '#{help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds.md', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')}';
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests')}';
window.gl.mrWidgetData.license_compliance_docs_path = '#{help_page_path('user/compliance/license_compliance/index.md', anchor: 'policies')}';
- window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}';
- window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}';
+ window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/approvals/rules.md', anchor: 'eligible-approvers')}';
+ window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/approvals/index.md")}';
window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}';
window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}';
window.gl.mrWidgetData.false_positive_doc_url = '#{help_page_path('user/application_security/vulnerabilities/index')}';
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
index a91751da0aa..2bd2c7cac44 100644
--- a/app/views/projects/mirrors/_instructions.html.haml
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -3,7 +3,7 @@
%li
= html_escape(_('The repository must be accessible over %{code_open}http://%{code_close},
%{code_open}https://%{code_close}, %{code_open}ssh://%{code_close} or %{code_open}git://%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- %li= html_escape(_('When using the %{code_open}http://%{code_close} or %{code_open}https://%{code_close} protocols, provide the exact URL to the repository. HTTP redirects will not be followed.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ %li= html_escape(_('When using the %{code_open}http://%{code_close} or %{code_open}https://%{code_close} protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li= html_escape(_('Include the username in the URL if required: %{code_open}https://username@gitlab.company.com/group/project.git%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li
- minutes = Gitlab.config.gitlab_shell.git_timeout / 60
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index c62853145b6..4e4738ebd25 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -12,7 +12,7 @@
.row{ 'v-cloak': true }
#blank-project-pane.tab-pane.active
- = form_for @project, html: { class: 'new_project' } do |f|
+ = form_for @project, html: { class: 'new_project gl-mt-3' } do |f|
= render 'new_project_fields', f: f, project_name_id: "blank-project-name"
#create-from-template-pane.tab-pane
diff --git a/app/views/projects/pages/_no_domains.html.haml b/app/views/projects/pages/_no_domains.html.haml
index 8d6e403b93a..a537bd80d30 100644
--- a/app/views/projects/pages/_no_domains.html.haml
+++ b/app/views/projects/pages/_no_domains.html.haml
@@ -3,4 +3,4 @@
.card-header
= s_('GitLabPages|Domains')
.nothing-here-block
- = s_("GitLabPages|Support for domains and certificates is disabled. Ask your system's administrator to enable it.")
+ = s_("GitLabPages|Support for domains and certificates is disabled. Ask your system's administrator to enable it.")
diff --git a/app/views/projects/pipeline_schedules/_tabs.html.haml b/app/views/projects/pipeline_schedules/_tabs.html.haml
index f69041e1eb1..f825ef35902 100644
--- a/app/views/projects/pipeline_schedules/_tabs.html.haml
+++ b/app/views/projects/pipeline_schedules/_tabs.html.haml
@@ -1,18 +1,12 @@
-%ul.nav-links.mobile-separator.nav.nav-tabs
- %li{ class: active_when(scope.nil?) }>
- = link_to schedule_path_proc.call(nil) do
- = s_("PipelineSchedules|All")
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm.js-totalbuilds-count
- = number_with_delimiter(all_schedules.count(:id))
+= gl_tabs_nav({ class: 'gl-display-flex gl-flex-grow-1 gl-border-0' }) do
+ = gl_tab_link_to schedule_path_proc.call(nil), { item_active: active_when(scope.nil?) } do
+ = s_("PipelineSchedules|All")
+ = gl_tab_counter_badge(number_with_delimiter(all_schedules.count(:id)), { class: 'js-totalbuilds-count' })
- %li{ class: active_when(scope == 'active') }>
- = link_to schedule_path_proc.call('active') do
- = s_("PipelineSchedules|Active")
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
- = number_with_delimiter(all_schedules.active.count(:id))
+ = gl_tab_link_to schedule_path_proc.call('active'), { item_active: active_when(scope == 'active') } do
+ = s_("PipelineSchedules|Active")
+ = gl_tab_counter_badge(number_with_delimiter(all_schedules.active.count(:id)))
- %li{ class: active_when(scope == 'inactive') }>
- = link_to schedule_path_proc.call('inactive') do
- = s_("PipelineSchedules|Inactive")
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
- = number_with_delimiter(all_schedules.inactive.count(:id))
+ = gl_tab_link_to schedule_path_proc.call('inactive'), { item_active: active_when(scope == 'inactive') } do
+ = s_("PipelineSchedules|Inactive")
+ = gl_tab_counter_badge(number_with_delimiter(all_schedules.inactive.count(:id)))
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 4a10f6aee1c..0bfdee088b4 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -17,45 +17,46 @@
- if @pipeline.queued_duration
= "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
- .well-segment.qa-pipeline-badges
- .icon-container
- = sprite_icon('flag')
- - if @pipeline.child?
- %span.js-pipeline-child.badge.badge-pill.gl-badge.sm.badge-primary.has-tooltip{ title: s_("Pipelines|This is a child pipeline within the parent pipeline") }
- = s_('Pipelines|Child pipeline')
- = surround '(', ')' do
- = link_to s_('Pipelines|parent'), pipeline_path(@pipeline.triggered_by_pipeline), class: 'text-white text-underline'
- - if @pipeline.latest?
- %span.js-pipeline-url-latest.badge.badge-pill.gl-badge.sm.badge-success.has-tooltip{ title: _("Latest pipeline for the most recent commit on this branch") }
- latest
- - if @pipeline.merge_train_pipeline?
- %span.js-pipeline-url-train.badge.badge-pill.gl-badge.sm.badge-info.has-tooltip{ title: _("This is a merge train pipeline") }
- train
- - if @pipeline.has_yaml_errors?
- %span.js-pipeline-url-yaml.badge.badge-pill.gl-badge.sm.badge-danger.has-tooltip{ title: @pipeline.yaml_errors }
- yaml invalid
- - if @pipeline.failure_reason?
- %span.js-pipeline-url-failure.badge.badge-pill.gl-badge.sm.badge-danger.has-tooltip{ title: @pipeline.failure_reason }
- error
- - if @pipeline.auto_devops_source?
- - popover_title_text = html_escape(_('This pipeline makes use of a predefined CI/CD configuration enabled by %{b_open}Auto DevOps.%{b_close}')) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe }
- - popover_content_url = help_page_path('topics/autodevops/index.md')
- - popover_content_text = _('Learn more about Auto DevOps')
- %a.js-pipeline-url-autodevops.badge.badge-pill.gl-badge.sm.badge-info.autodevops-badge{ href: "#", tabindex: "0", role: "button", data: { container: "body",
- toggle: "popover",
- placement: "top",
- html: "true",
- triggers: "focus",
- title: "<div class='gl-font-weight-normal gl-line-height-normal'>#{popover_title_text}</div>",
- content: "<a href='#{popover_content_url}' target='_blank' rel='noopener noreferrer nofollow'>#{popover_content_text}</a>",
- } }
- Auto DevOps
- - if @pipeline.detached_merge_request_pipeline?
- %span.js-pipeline-url-mergerequest.badge.badge-pill.gl-badge.sm.badge-info.has-tooltip{ title: _('Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.') }
- detached
- - if @pipeline.stuck?
- %span.js-pipeline-url-stuck.badge.badge-pill.gl-badge.sm.badge-warning
- stuck
+ - if has_pipeline_badges?(@pipeline)
+ .well-segment.qa-pipeline-badges
+ .icon-container
+ = sprite_icon('flag')
+ - if @pipeline.child?
+ %span.js-pipeline-child.badge.badge-pill.gl-badge.sm.badge-primary.has-tooltip{ title: s_("Pipelines|This is a child pipeline within the parent pipeline") }
+ = s_('Pipelines|Child pipeline')
+ = surround '(', ')' do
+ = link_to s_('Pipelines|parent'), pipeline_path(@pipeline.triggered_by_pipeline), class: 'text-white text-underline'
+ - if @pipeline.latest?
+ %span.js-pipeline-url-latest.badge.badge-pill.gl-badge.sm.badge-success.has-tooltip{ title: _("Latest pipeline for the most recent commit on this branch") }
+ latest
+ - if @pipeline.merge_train_pipeline?
+ %span.js-pipeline-url-train.badge.badge-pill.gl-badge.sm.badge-info.has-tooltip{ title: _("This is a merge train pipeline") }
+ train
+ - if @pipeline.has_yaml_errors?
+ %span.js-pipeline-url-yaml.badge.badge-pill.gl-badge.sm.badge-danger.has-tooltip{ title: @pipeline.yaml_errors }
+ yaml invalid
+ - if @pipeline.failure_reason?
+ %span.js-pipeline-url-failure.badge.badge-pill.gl-badge.sm.badge-danger.has-tooltip{ title: @pipeline.failure_reason }
+ error
+ - if @pipeline.auto_devops_source?
+ - popover_title_text = html_escape(_('This pipeline makes use of a predefined CI/CD configuration enabled by %{b_open}Auto DevOps.%{b_close}')) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe }
+ - popover_content_url = help_page_path('topics/autodevops/index.md')
+ - popover_content_text = _('Learn more about Auto DevOps')
+ %a.js-pipeline-url-autodevops.badge.badge-pill.gl-badge.sm.badge-info.autodevops-badge{ href: "#", tabindex: "0", role: "button", data: { container: "body",
+ toggle: "popover",
+ placement: "top",
+ html: "true",
+ triggers: "focus",
+ title: "<div class='gl-font-weight-normal gl-line-height-normal'>#{popover_title_text}</div>",
+ content: "<a href='#{popover_content_url}' target='_blank' rel='noopener noreferrer nofollow'>#{popover_content_text}</a>",
+ } }
+ Auto DevOps
+ - if @pipeline.detached_merge_request_pipeline?
+ %span.js-pipeline-url-mergerequest.badge.badge-pill.gl-badge.sm.badge-info.has-tooltip{ title: _('Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.') }
+ detached
+ - if @pipeline.stuck?
+ %span.js-pipeline-url-stuck.badge.badge-pill.gl-badge.sm.badge-warning
+ stuck
.well-segment.branch-info
.icon-container.commit-icon
diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml
index 992407adf71..21e871246d7 100644
--- a/app/views/projects/pipelines/charts.html.haml
+++ b/app/views/projects/pipelines/charts.html.haml
@@ -1,4 +1,5 @@
- page_title _('CI/CD Analytics')
#js-project-pipelines-charts-app{ data: { project_path: @project.full_path,
- should_render_dora_charts: should_render_dora_charts.to_s } }
+ should_render_dora_charts: should_render_dora_charts.to_s,
+ should_render_quality_summary: should_render_quality_summary.to_s } }
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 8fd8d3cf540..c911fc8a203 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -22,8 +22,8 @@
%ul
- @pipeline.yaml_errors.split(",").each do |error|
%li= error
- - lint_link_url = project_ci_lint_path(@project)
- - lint_link_start = '<a href="%{url}">'.html_safe % { url: lint_link_url }
+ - lint_link_url = project_ci_pipeline_editor_path(@project, tab: "LINT_TAB")
+ - lint_link_start = '<a href="%{url}" class="gl-text-blue-500!">'.html_safe % { url: lint_link_url }
= s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
= render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
diff --git a/app/views/projects/product_analytics/_links.html.haml b/app/views/projects/product_analytics/_links.html.haml
index 0797c5baf91..6e5667e2644 100644
--- a/app/views/projects/product_analytics/_links.html.haml
+++ b/app/views/projects/product_analytics/_links.html.haml
@@ -1,10 +1,5 @@
-.mb-3
- %ul.nav-links
- = nav_link(path: 'product_analytics#index') do
- = link_to _('Events'), project_product_analytics_path(@project)
- = nav_link(path: 'product_analytics#graphs') do
- = link_to 'Graphs', graphs_project_product_analytics_path(@project)
- = nav_link(path: 'product_analytics#test') do
- = link_to _('Test'), test_project_product_analytics_path(@project)
- = nav_link(path: 'product_analytics#setup') do
- = link_to _('Setup'), setup_project_product_analytics_path(@project)
+= gl_tabs_nav({ class: 'mb-3'}) do
+ = gl_tab_link_to _('Events'), project_product_analytics_path(@project)
+ = gl_tab_link_to _('Graphs'), graphs_project_product_analytics_path(@project)
+ = gl_tab_link_to _('Test'), test_project_product_analytics_path(@project)
+ = gl_tab_link_to _('Setup'), setup_project_product_analytics_path(@project)
diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml
index b185f45d129..0f4d5869cea 100644
--- a/app/views/projects/settings/_general.html.haml
+++ b/app/views/projects/settings/_general.html.haml
@@ -1,3 +1,4 @@
+- hidden_topics_field_id = 'project_topic_list_field'
= form_for [@project], html: { multipart: true, class: "edit-project js-general-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-general-settings' }
@@ -15,9 +16,9 @@
.row
.form-group.col-md-9
- = f.label :topics, _('Topics (optional)'), class: 'label-bold'
- = f.text_field :topics, value: @project.topic_list.join(', '), maxlength: 2000, class: "form-control gl-form-input"
- %p.form-text.text-muted= _('Separate topics with commas.')
+ = f.label :topics, _('Topics'), class: 'label-bold'
+ .js-topics-selector{ data: { hidden_input_id: hidden_topics_field_id } }
+ = f.hidden_field :topics, value: @project.topic_list.join(', '), id: hidden_topics_field_id
.row
.form-group.col-md-9
diff --git a/app/views/projects/work_items/index.html.haml b/app/views/projects/work_items/index.html.haml
index 052db598571..0efd7a740d3 100644
--- a/app/views/projects/work_items/index.html.haml
+++ b/app/views/projects/work_items/index.html.haml
@@ -1,3 +1,3 @@
- page_title s_('WorkItem|Work Items')
-#js-work-items
+#js-work-items{ data: { full_path: @project.full_path } }
diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml
index 9356b6ad49c..65a1ffa3e46 100644
--- a/app/views/registrations/welcome/show.html.haml
+++ b/app/views/registrations/welcome/show.html.haml
@@ -24,9 +24,10 @@
.form-group.col-sm-12.js-other-role-group.hidden
= f.label :other_role, _('What is your job title? (optional)'), class: 'form-check-label gl-mb-3'
= f.text_field :other_role, class: 'form-control'
+ = render_if_exists "registrations/welcome/jobs_to_be_done", f: f
= render_if_exists "registrations/welcome/setup_for_company", f: f
+ = render_if_exists "registrations/welcome/joining_project"
= render 'devise/shared/email_opted_in', f: f
- = render_if_exists "registrations/welcome/jobs_to_be_done", f: f
.row
.form-group.col-sm-12.gl-mb-0
- if partial_exists? "registrations/welcome/button"
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index ca6f2369bd8..54aa9aad8a5 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -5,7 +5,7 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
- %ul.nav-links.search-filter.scrolling-tabs.nav.nav-tabs
+ = gl_tabs_nav({ class: 'search-filter scrolling-tabs nav-links'}) do
- if @project
- if project_search_tabs?(:blobs)
= search_filter_link 'blobs', _("Code"), data: { qa_selector: 'code_tab' }
diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml
index 8b13bb948ee..4cb3f6d1739 100644
--- a/app/views/shared/_confirm_modal.html.haml
+++ b/app/views/shared/_confirm_modal.html.haml
@@ -12,10 +12,10 @@
%p
%span.js-warning-text= _('This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.')
%br
- - phrase_code = '<code class="js-confirm-danger-match">%{phrase_name}</code>'.html_safe % { phrase_name: phrase }
+ - phrase_code = '<code class="js-legacy-confirm-danger-match">%{phrase_name}</code>'.html_safe % { phrase_name: phrase }
= _('Please type %{phrase_code} to proceed or close this modal to cancel.').html_safe % { phrase_code: phrase_code }
.form-group
- = text_field_tag 'confirm_name_input', '', class: 'form-control js-confirm-danger-input qa-confirm-input'
+ = text_field_tag 'confirm_name_input', '', class: 'form-control js-legacy-confirm-danger-input qa-confirm-input'
.form-actions
- = submit_tag _('Confirm'), class: "gl-button btn btn-danger js-confirm-danger-submit qa-confirm-button"
+ = submit_tag _('Confirm'), class: "gl-button btn btn-danger js-legacy-confirm-danger-submit qa-confirm-button"
diff --git a/app/views/shared/_default_branch_protection.html.haml b/app/views/shared/_default_branch_protection.html.haml
index d7ae21debd8..7a6152f6d96 100644
--- a/app/views/shared/_default_branch_protection.html.haml
+++ b/app/views/shared/_default_branch_protection.html.haml
@@ -1,3 +1,4 @@
-.form-group
- = f.label :default_branch_protection, class: 'label-bold'
- = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, selected_level), {}, class: 'form-control'
+%fieldset.form-group
+ %legend.h5.gl-border-none.gl-mt-0.gl-mb-3= _('Default branch protection')
+ - Gitlab::Access.protection_options.each do |option|
+ = f.gitlab_ui_radio_component :default_branch_protection, option[:value], option[:label], help_text: option[:help_text]
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 6eb736b0710..4e3b1e02f16 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,4 +1,4 @@
-= render 'shared/alerts/positioning_disabled'
+= render 'shared/alerts/positioning_disabled' if @sort == 'relative_position'
- if @issues.to_a.any?
%ul.content-list.issues-list.issuable-list{ class: issue_manual_ordering_class, data: { group_full_path: @group&.full_path } }
diff --git a/app/views/shared/_md_preview.html.haml b/app/views/shared/_md_preview.html.haml
index 0976defea1b..a49a0667d84 100644
--- a/app/views/shared/_md_preview.html.haml
+++ b/app/views/shared/_md_preview.html.haml
@@ -9,7 +9,7 @@
.md-area.position-relative
.md-header
- %ul.nav.nav-tabs.nav-links.clearfix
+ = gl_tabs_nav({ class: 'clearfix nav-links'}) do
%li.md-header-tab.active
%button.js-md-write-button
= _("Write")
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
index 5641c67e462..14ea96f9669 100644
--- a/app/views/shared/_new_commit_form.html.haml
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -5,9 +5,7 @@
- if project.empty_repo?
- ref = local_assigns[:ref] || @ref
- - branch_name_class = project.empty_repo_upload_experiment? ? 'js-branch-name' : nil
-
- = hidden_field_tag 'branch_name', ref, class: branch_name_class
+ = hidden_field_tag 'branch_name', ref, class: 'js-branch-name'
- else
- if can?(current_user, :push_code, @project)
.form-group.row.branch
diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml
index 9c1e5a49b44..d1e1a8a819d 100644
--- a/app/views/shared/_no_password.html.haml
+++ b/app/views/shared/_no_password.html.haml
@@ -1,12 +1,10 @@
- if show_no_password_message?
- .no-password-message.gl-alert.gl-alert-warning
- = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label': _('Dismiss') }
- = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ = render 'shared/global_alert',
+ variant: :warning,
+ alert_class: 'js-no-password-message',
+ close_button_class: 'js-hide-no-password-message' do
.gl-alert-body
- - translation_params = { protocol: gitlab_config.protocol.upcase, set_password_link: link_to_set_password }
- - set_password_message = _("You won't be able to pull or push repositories via %{protocol} until you %{set_password_link} on your account") % translation_params
- = set_password_message.html_safe
+ = no_password_message
.gl-alert-actions
- = link_to _('Remind later'), '#', class: 'hide-no-password-message btn gl-alert-action btn-info btn-md gl-button'
- = link_to _("Don't show again"), profile_path(user: {hide_no_password: true}), method: :put, role: 'button', class: 'btn gl-alert-action btn-md btn-default gl-button btn-default-secondary'
+ = link_to _('Remind later'), '#', class: 'js-hide-no-password-message gl-alert-action btn btn-confirm btn-md gl-button'
+ = link_to _("Don't show again"), profile_path(user: { hide_no_password: true }), method: :put, role: 'button', class: 'gl-alert-action btn btn-default btn-md gl-button'
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index 2c6ceb58654..20dc1b41970 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -1,10 +1,10 @@
- if show_no_ssh_key_message?
- %div{ class: 'no-ssh-key-message gl-alert gl-alert-warning', role: 'alert' }
- = sprite_icon('warning', css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title')
- %button{ class: 'gl-alert-dismiss hide-no-ssh-message', type: 'button', 'aria-label': _('Dismiss') }
- = sprite_icon('close', css_class: 'gl-icon s16')
+ = render 'shared/global_alert',
+ variant: :warning,
+ alert_class: 'js-no-ssh-message',
+ close_button_class: 'js-hide-no-ssh-message' do
.gl-alert-body
- = s_("MissingSSHKeyWarningLink|You won't be able to pull or push repositories via SSH until you add an SSH key to your profile")
+ = s_("MissingSSHKeyWarningLink|You can't push or pull repositories using SSH until you add an SSH key to your profile.")
.gl-alert-actions
- = link_to s_('MissingSSHKeyWarningLink|Add SSH key'), profile_keys_path, class: "btn gl-alert-action btn-warning btn-md gl-button"
- = link_to s_("MissingSSHKeyWarningLink|Don't show again"), profile_path(user: {hide_no_ssh_key: true}), method: :put, role: 'button', class: 'btn gl-alert-action btn-md btn-warning gl-button btn-warning-secondary'
+ = link_to s_('MissingSSHKeyWarningLink|Add SSH key'), profile_keys_path, class: "gl-alert-action btn btn-confirm btn-md gl-button"
+ = link_to s_("MissingSSHKeyWarningLink|Don't show again"), profile_path(user: { hide_no_ssh_key: true }), method: :put, role: 'button', class: 'gl-alert-action btn btn-default btn-md gl-button'
diff --git a/app/views/shared/_project_creation_levels.html.haml b/app/views/shared/_project_creation_levels.html.haml
new file mode 100644
index 00000000000..00f495a26dc
--- /dev/null
+++ b/app/views/shared/_project_creation_levels.html.haml
@@ -0,0 +1,7 @@
+- method = local_assigns.fetch(:method, nil)
+- legend = local_assigns.fetch(:legend, nil)
+
+%fieldset.form-group
+ %legend.h5.gl-border-none.gl-mt-0.gl-mb-3= legend
+ - Gitlab::Access.project_creation_options.each do |label, value|
+ = f.gitlab_ui_radio_component method, value, label
diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml
index f48bfcd0e72..760fe18ddec 100644
--- a/app/views/shared/_visibility_radios.html.haml
+++ b/app/views/shared/_visibility_radios.html.haml
@@ -10,6 +10,7 @@
= visibility_level_label(level)
.option-description
= visibility_level_description(level, form_model)
+ .option-disabled-reason
.text-muted
- if all_visibility_levels_restricted?
diff --git a/app/views/shared/alerts/_positioning_disabled.html.haml b/app/views/shared/alerts/_positioning_disabled.html.haml
index 91c1d3463d8..59f2fcef24c 100644
--- a/app/views/shared/alerts/_positioning_disabled.html.haml
+++ b/app/views/shared/alerts/_positioning_disabled.html.haml
@@ -1,2 +1,2 @@
- if issue_repositioning_disabled?
- = render 'shared/alert_info', body: _('Issues manual ordering is temporarily disabled for technical reasons.')
+ = render 'shared/alert_info', body: _('Issues are being rebalanced at the moment, so manual reordering is disabled.')
diff --git a/app/views/shared/boards/_switcher.html.haml b/app/views/shared/boards/_switcher.html.haml
index 18e0ca20cf7..c667b3a4626 100644
--- a/app/views/shared/boards/_switcher.html.haml
+++ b/app/views/shared/boards/_switcher.html.haml
@@ -3,15 +3,10 @@
- milestone_filter_opts = milestone_filter_opts.merge(only_group_milestones: true) if board.group_board?
- weights = Gitlab.ee? ? ([Issue::WEIGHT_ANY] + Issue.weight_options) : []
-#js-multiple-boards-switcher.inline.boards-switcher{ data: { current_board: current_board_json.to_json,
- milestone_path: milestones_filter_path(milestone_filter_opts),
+#js-multiple-boards-switcher.inline.boards-switcher{ data: { milestone_path: milestones_filter_path(milestone_filter_opts),
board_base_url: board_base_url,
has_missing_boards: (!multiple_boards_available? && current_board_parent.boards.size > 1).to_s,
can_admin_board: can?(current_user, :admin_issue_board, parent).to_s,
multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s,
- labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: true),
- labels_web_url: parent.is_a?(Project) ? project_labels_path(@project) : group_labels_path(@group),
- project_id: @project&.id,
- group_id: @group&.id,
scoped_issue_board_feature_enabled: Gitlab.ee? && parent.feature_available?(:scoped_issue_board) ? 'true' : 'false',
weights: weights.to_json } }
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index e049afbc40b..902a0cad483 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -36,7 +36,7 @@
.text-secondary= s_('DeployTokens|Allows read-only access to registry images.')
%fieldset.form-group.form-check
- = f.check_box :write_registry, class: 'form-check-input'
+ = f.check_box :write_registry, class: 'form-check-input', data: { qa_selector: 'deploy_token_write_registry_checkbox' }
= f.label :write_registry, 'write_registry', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows read and write access to registry images.')
diff --git a/app/views/shared/doorkeeper/applications/_show.html.haml b/app/views/shared/doorkeeper/applications/_show.html.haml
index 8d6b9604c1c..f533b5b5a4d 100644
--- a/app/views/shared/doorkeeper/applications/_show.html.haml
+++ b/app/views/shared/doorkeeper/applications/_show.html.haml
@@ -39,6 +39,9 @@
= render "shared/tokens/scopes_list", token: @application
-.form-actions
- = link_to _('Edit'), edit_path, class: 'gl-button btn btn-confirm wide float-left'
- = render 'shared/doorkeeper/applications/delete_form', path: delete_path, submit_btn_css: 'gl-button btn btn-danger gl-ml-3'
+.form-actions.gl-display-flex.gl-justify-content-space-between
+ %div
+ - if @created
+ = link_to _('Continue'), index_path, class: 'btn btn-confirm btn-md gl-button gl-mr-3'
+ = link_to _('Edit'), edit_path, class: 'btn btn-default btn-md gl-button'
+ = render 'shared/doorkeeper/applications/delete_form', path: delete_path, submit_btn_css: 'btn btn-danger btn-md gl-button btn-danger-secondary'
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
index 51f44afe55e..ab6875a09f7 100644
--- a/app/views/shared/hook_logs/_content.html.haml
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -1,44 +1,47 @@
-%p
- %strong Request URL:
- POST
- = hook_log.url
- = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
+%span.gl-display-flex.gl-align-items-center
+ %h4
+ POST
+ = hook_log.url
+ %span.badge.gl-badge.badge-pill.badge-muted.sm.gl-ml-3
+ = hook_log.trigger.singularize.titleize
%p
- %strong Trigger:
- %td.d-none.d-sm-block
- %span.badge.badge-gray.deploy-project-label
- = hook_log.trigger.singularize.titleize
-%p
- %strong Elapsed time:
- #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
-%p
- %strong Request time:
- = time_ago_with_tooltip(hook_log.created_at)
+ = _('Completed in %{duration_seconds} seconds (%{relative_time})').html_safe % { duration_seconds: number_with_precision(hook_log.execution_duration, precision: 2), relative_time: time_ago_with_tooltip(hook_log.created_at) }
%hr
- if hook_log.internal_error_message.present?
- .bs-callout.bs-callout-danger
- = hook_log.internal_error_message
+ .gl-alert-container
+ .gl-alert.gl-alert-danger
+ .gl-alert-container
+ = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-content
+ %h4.gl-alert-title= _('Internal error occured while delivering this webhook.')
+ .gl-alert-body
+ = _('Error: %{error}') % { error: hook_log.internal_error_message }
+
+%h4= _('Response')
+= render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
+%pre.gl-mt-3
+ :escaped
+ #{hook_log.response_body}
-%h5 Request headers:
+%h5= _('Headers')
%pre
- - hook_log.request_headers.each do |k, v|
- <strong>#{k}:</strong> #{v}
+ - hook_log.response_headers.each do |k, v|
+ <span class="gl-font-weight-bold">#{k}:</span> #{v}
%br
-%h5 Request body:
+%h4.gl-mt-6= _('Request')
%pre
:escaped
#{Gitlab::Json.pretty_generate(hook_log.request_data)}
-%h5 Response headers:
+
+%h5= _('Headers')
%pre
- - hook_log.response_headers.each do |k, v|
- <strong>#{k}:</strong> #{v}
+ - hook_log.request_headers.each do |k, v|
+ <span class="gl-font-weight-bold">#{k}:</span> #{v}
%br
-%h5 Response body:
-%pre
- :escaped
- #{hook_log.response_body}
+
+
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 81a7581d392..e097852216c 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -15,6 +15,7 @@
.d-flex.flex-column.flex-md-row.flex-grow-1.mb-lg-0.mb-md-2.mb-sm-0.w-100
- if type == :boards
= render "shared/boards/switcher", board: board
+ .js-new-board{ data: { multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s, can_admin_board: can?(current_user, :admin_issue_board, parent).to_s, } }
= form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index 1f391e8a321..46df9b4ef9a 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -23,7 +23,7 @@
= html_escape(_('From %{code_open}%{source_title}%{code_close} into')) % { source_title: source_title, code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- if issuable.new_record?
- %code#js-target-branch-title= target_title
+ %code#js-target-branch-title{ data: { branch_name: @merge_request.target_branch } }= target_title
&nbsp;
= link_to _('Change branches'), mr_change_branches_path(issuable)
- elsif issuable.for_fork?
diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml
index f5f6f32d5ba..ae0fe54de4f 100644
--- a/app/views/shared/issuable/form/_type_selector.html.haml
+++ b/app/views/shared/issuable/form/_type_selector.html.haml
@@ -18,17 +18,19 @@
= sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
.dropdown-content{ data: { testid: 'issue-type-select-dropdown' } }
%ul
- %li.js-filter-issuable-type
- = link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
- #{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_("Issue")}
- %li.js-filter-issuable-type{ data: { track: { action: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } }
- = link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do
- #{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_("Incident")}
+ - if create_issue_type_allowed?(@project, :issue)
+ %li.js-filter-issuable-type
+ = link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
+ #{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_('Issue')}
+ - if create_issue_type_allowed?(@project, :incident)
+ %li.js-filter-issuable-type{ data: { track: { action: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } }
+ = link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do
+ #{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_('Incident')}
#js-type-popover
- if issuable.incident?
%p.form-text.text-muted
- incident_docs_url = help_page_path('operations/incident_management/incidents.md')
- - incident_docs_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: incident_docs_url }
- = _('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.').html_safe % { incident_docs_start: incident_docs_start, incident_docs_end: '</a>'.html_safe }
+ - incident_docs_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: incident_docs_url)
+ = format(_('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.'), incident_docs_start: incident_docs_start, incident_docs_end: '</a>').html_safe
diff --git a/app/views/shared/labels/_nav.html.haml b/app/views/shared/labels/_nav.html.haml
index a0225e35c90..47e9d9b0e4a 100644
--- a/app/views/shared/labels/_nav.html.haml
+++ b/app/views/shared/labels/_nav.html.haml
@@ -1,14 +1,10 @@
- subscribed = params[:subscribed]
.top-area.adjust
- %ul.nav-links.nav.nav-tabs
- %li{ class: active_when(subscribed != 'true') }>
- = link_to labels_filter_path do
- = _('All')
+ = gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do
+ = gl_tab_link_to _('All'), labels_filter_path, { item_active: subscribed != 'true' }
- if current_user
- %li{ class: active_when(subscribed == 'true') }>
- = link_to labels_filter_path(subscribed: 'true') do
- = _('Subscribed')
+ = gl_tab_link_to _('Subscribed'), labels_filter_path(subscribed: 'true'), { item_active: subscribed == 'true' }
.nav-controls
= form_tag labels_filter_path, method: :get do
= hidden_field_tag :subscribed, params[:subscribed]
diff --git a/app/views/shared/nav/_sidebar_menu.html.haml b/app/views/shared/nav/_sidebar_menu.html.haml
index 903d2d077ba..3f71368aff3 100644
--- a/app/views/shared/nav/_sidebar_menu.html.haml
+++ b/app/views/shared/nav/_sidebar_menu.html.haml
@@ -16,15 +16,4 @@
%span.badge.badge-pill.count{ **sidebar_menu.pill_html_options }
= number_with_delimiter(sidebar_menu.pill_count)
- %ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_renderable_items?) }
- = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' } ) do
- %span.fly-out-top-item-container
- %strong.fly-out-top-item-name
- = sidebar_menu.title
- - if sidebar_menu.has_pill?
- %span.badge.badge-pill.count.fly-out-badge{ **sidebar_menu.pill_html_options }
- = number_with_delimiter(sidebar_menu.pill_count)
-
- - if sidebar_menu.has_renderable_items?
- %li.divider.fly-out-top-item
- = render partial: 'shared/nav/sidebar_menu_item', collection: sidebar_menu.renderable_items
+ = render partial: 'shared/nav/sidebar_submenu', locals: { sidebar_menu: sidebar_menu }
diff --git a/app/views/shared/nav/_sidebar_submenu.html.haml b/app/views/shared/nav/_sidebar_submenu.html.haml
new file mode 100644
index 00000000000..750e6c9ee57
--- /dev/null
+++ b/app/views/shared/nav/_sidebar_submenu.html.haml
@@ -0,0 +1,12 @@
+%ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_renderable_items?) }
+ = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' } ) do
+ %span.fly-out-top-item-container
+ %strong.fly-out-top-item-name
+ = sidebar_menu.title
+ - if sidebar_menu.has_pill?
+ %span.badge.badge-pill.count.fly-out-badge{ **sidebar_menu.pill_html_options }
+ = number_with_delimiter(sidebar_menu.pill_count)
+
+ - if sidebar_menu.has_renderable_items?
+ %li.divider.fly-out-top-item
+ = render partial: 'shared/nav/sidebar_menu_item', collection: sidebar_menu.renderable_items
diff --git a/app/views/shared/projects/_topics.html.haml b/app/views/shared/projects/_topics.html.haml
index a7429483da1..e303f05c5df 100644
--- a/app/views/shared/projects/_topics.html.haml
+++ b/app/views/shared/projects/_topics.html.haml
@@ -8,7 +8,7 @@
= sprite_icon('tag', css_class: 'icon gl-relative gl-mr-2')
- project.topics_to_show.each do |topic|
- - explore_project_topic_path = explore_projects_path(topic: topic)
+ - explore_project_topic_path = topic_explore_projects_path(topic_name: topic)
- if topic.length > max_project_topic_length
%a{ class: "#{ project_topics_classes } str-truncated-30 has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' }
= truncate(topic, length: max_project_topic_length)
@@ -21,7 +21,7 @@
- content = capture do
%span.gl-display-inline-flex.gl-flex-wrap
- project.topics_not_shown.each do |topic|
- - explore_project_topic_path = explore_projects_path(topic: topic)
+ - explore_project_topic_path = topic_explore_projects_path(topic_name: topic)
- if topic.length > max_project_topic_length
%a{ class: "#{ project_topics_classes } gl-mb-3 str-truncated has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' }
= truncate(topic, length: max_project_topic_length)
diff --git a/app/views/shared/runners/_shared_runners_description.html.haml b/app/views/shared/runners/_shared_runners_description.html.haml
index a276f725576..e2b57a7fd73 100644
--- a/app/views/shared/runners/_shared_runners_description.html.haml
+++ b/app/views/shared/runners/_shared_runners_description.html.haml
@@ -1,4 +1,5 @@
-- link = link_to _('MaxBuilds'), 'https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersmachine-section', target: '_blank'
+-# "MaxBuilds" is a runner configuration keyword so it must not be translated.
+- link = link_to 'MaxBuilds', 'https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersmachine-section', target: '_blank'
%h4
= _('Shared runners')
diff --git a/app/views/shared/topics/_list.html.haml b/app/views/shared/topics/_list.html.haml
new file mode 100644
index 00000000000..ddf47261d42
--- /dev/null
+++ b/app/views/shared/topics/_list.html.haml
@@ -0,0 +1,9 @@
+- remote = local_assigns.fetch(:remote, false)
+
+- if @topics.empty?
+ = render 'shared/empty_states/topics'
+- else
+ .row.gl-mt-3
+ = render partial: 'shared/topics/topic', collection: @topics
+
+ = paginate_collection @topics, remote: remote
diff --git a/app/views/shared/topics/_topic.html.haml b/app/views/shared/topics/_topic.html.haml
new file mode 100644
index 00000000000..a47d4495777
--- /dev/null
+++ b/app/views/shared/topics/_topic.html.haml
@@ -0,0 +1,16 @@
+- max_topic_name_length = 30
+- detail_page_link = topic_explore_projects_path(topic_name: topic.name)
+
+.col-lg-3.col-md-4.col-sm-12
+ .gl-card.gl-mb-5
+ .gl-card-body.gl-display-flex.gl-align-items-center
+ .avatar-container.rect-avatar.s40.gl-flex-shrink-0
+ = link_to detail_page_link do
+ = topic_icon(topic, class: "avatar s40")
+ = link_to detail_page_link do
+ - if topic.name.length > max_topic_name_length
+ %h5.str-truncated.has-tooltip{ title: topic.name }
+ = truncate(topic.name, length: max_topic_name_length)
+ - else
+ %h5
+ = topic.name
diff --git a/app/views/shared/web_hooks/_index.html.haml b/app/views/shared/web_hooks/_index.html.haml
index 794418b8336..f1eef5d7f0f 100644
--- a/app/views/shared/web_hooks/_index.html.haml
+++ b/app/views/shared/web_hooks/_index.html.haml
@@ -3,7 +3,7 @@
.card-header
%h5
= hook_class.underscore.humanize.titleize.pluralize
- (#{hooks.load.size})
+ (#{hooks.size})
- if hooks.any?
%ul.content-list
diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml
index 2e6d335a98d..0d904de9372 100644
--- a/app/views/users/_overview.html.haml
+++ b/app/views/users/_overview.html.haml
@@ -2,13 +2,28 @@
.row.d-none.d-sm-flex
.col-12.calendar-block.gl-my-3
- .user-calendar.light{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
+ .user-calendar.light{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: local_time_instance(@user.timezone).now.utc_offset } }
.gl-spinner.gl-spinner-md.gl-my-8
.user-calendar-error.invisible
= _('There was an error loading users activity calendar.')
%a.js-retry-load{ href: '#' }
= s_('UserProfile|Retry')
.user-calendar-activities
+- if @user.user_readme
+ .row.justify-content-center
+ .col-12.col-md-10.col-lg-8.gl-my-6
+ .gl-display-flex
+ %ol.breadcrumb.gl-breadcrumb-list.gl-mb-4
+ %li.breadcrumb-item.gl-breadcrumb-item
+ = link_to @user.username, project_path(@user.user_project)
+ %span.gl-breadcrumb-separator
+ = sprite_icon("chevron-right", size: 16)
+ %li.breadcrumb-item.gl-breadcrumb-item
+ = link_to @user.user_readme.path, @user.user_project.readme_url
+ - if current_user == @user
+ .gl-ml-auto
+ = link_to _('Edit'), edit_blob_path(@user.user_project, @user.user_project.default_branch, @user.user_readme.path)
+ = render 'projects/blob/viewer', viewer: @user.user_readme.rich_viewer, load_async: false
.row
%div{ class: activity_pane_class }
- if can?(current_user, :read_cross_project)
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 2f44a57c388..cffeb55597f 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -5,9 +5,9 @@
%ul.bordered-list
- @events.sort_by(&:created_at).each do |event|
%li
- %span.light
+ %span.light.js-localtime{ :data => { :datetime => event.created_at.utc.strftime('%Y-%m-%dT%H:%M:%SZ'), :toggle => 'tooltip', :placement => 'top' } }
= sprite_icon('clock', css_class: 'gl-vertical-align-text-bottom')
- = event.created_at.to_time.in_time_zone.strftime('%-I:%M%P')
+ = event.created_at.to_time.in_time_zone(@user.timezone).strftime('%-I:%M%P')
- if event.visible_to_user?(current_user)
- if event.push_action?
#{event.action_name} #{event.ref_type}
diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml
index 771ee693120..92095e78f69 100644
--- a/app/views/users/terms/index.html.haml
+++ b/app/views/users/terms/index.html.haml
@@ -1,20 +1,23 @@
- redirect_params = { redirect: @redirect } if @redirect
- accept_term_link = accept_term_path(@term, redirect_params)
-.card-body.rendered-terms{ data: { qa_selector: 'terms_content' } }
- = markdown_field(@term, :terms)
-- if current_user
- = render_if_exists 'devise/shared/form_phone_verification', accept_term_link: accept_term_link, inline: true
- .card-footer.footer-block.clearfix
- - if can?(current_user, :accept_terms, @term)
- .float-right
- = button_to accept_term_link, class: 'gl-button btn btn-confirm gl-ml-3', data: { qa_selector: 'accept_terms_button' } do
- = _('Accept terms')
- - else
- .float-right
- = link_to root_path, class: 'gl-button btn btn-confirm gl-ml-3' do
- = _('Continue')
- - if can?(current_user, :decline_terms, @term)
- .float-right
- = button_to decline_term_path(@term, redirect_params), class: 'gl-button btn btn-default gl-ml-3' do
- = _('Decline and sign out')
+- if Feature.enabled?(:terms_of_service_vue, current_user, default_enabled: :yaml)
+ #js-terms-of-service{ data: { terms_data: terms_data(@term, @redirect) } }
+- else
+ .card-body.rendered-terms{ data: { qa_selector: 'terms_content' } }
+ = markdown_field(@term, :terms)
+ - if current_user
+ = render_if_exists 'devise/shared/form_phone_verification', accept_term_link: accept_term_link, inline: true
+ .card-footer.footer-block.clearfix
+ - if can?(current_user, :accept_terms, @term)
+ .float-right
+ = button_to accept_term_link, class: 'gl-button btn btn-confirm gl-ml-3', data: { qa_selector: 'accept_terms_button' } do
+ = _('Accept terms')
+ - else
+ .float-right
+ = link_to root_path, class: 'gl-button btn btn-confirm gl-ml-3' do
+ = _('Continue')
+ - if can?(current_user, :decline_terms, @term)
+ .float-right
+ = button_to decline_term_path(@term, redirect_params), class: 'gl-button btn btn-default gl-ml-3' do
+ = _('Decline and sign out')
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index c7ce2eb8d00..699744b355c 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -246,6 +246,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: cronjob:clusters_integrations_check_prometheus_health
+ :worker_name: Clusters::Integrations::CheckPrometheusHealthWorker
+ :feature_category: :incident_management
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:container_expiration_policy
:worker_name: ContainerExpirationPolicyWorker
:feature_category: :container_registry
@@ -347,13 +356,22 @@
:tags: []
- :name: cronjob:issue_due_scheduler
:worker_name: IssueDueSchedulerWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:tags: []
+- :name: cronjob:issues_reschedule_stuck_issue_rebalances
+ :worker_name: Issues::RescheduleStuckIssueRebalancesWorker
+ :feature_category: :team_planning
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:jira_import_stuck_jira_import_jobs
:worker_name: Gitlab::JiraImport::StuckJiraImportJobsWorker
:feature_category: :importers
@@ -363,6 +381,15 @@
:weight: 1
:idempotent:
:tags: []
+- :name: cronjob:loose_foreign_keys_cleanup
+ :worker_name: LooseForeignKeys::CleanupWorker
+ :feature_category: :sharding
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:member_invitation_reminder_emails
:worker_name: MemberInvitationReminderEmailsWorker
:feature_category: :subgroups
@@ -696,6 +723,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: deployment:deployments_archive_in_project
+ :worker_name: Deployments::ArchiveInProjectWorker
+ :feature_category: :continuous_delivery
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 3
+ :idempotent: true
+ :tags: []
- :name: deployment:deployments_drop_older_deployments
:worker_name: Deployments::DropOlderDeploymentsWorker
:feature_category: :continuous_delivery
@@ -1069,15 +1105,6 @@
:idempotent:
:tags:
- :needs_own_queue
-- :name: incident_management:clusters_applications_check_prometheus_health
- :worker_name: Clusters::Applications::CheckPrometheusHealthWorker
- :feature_category: :incident_management
- :has_external_dependencies: true
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 2
- :idempotent: true
- :tags: []
- :name: incident_management:incident_management_add_severity_system_note
:worker_name: IncidentManagement::AddSeveritySystemNoteWorker
:feature_category: :incident_management
@@ -1251,7 +1278,7 @@
:tags: []
- :name: mail_scheduler:mail_scheduler_issue_due
:worker_name: MailScheduler::IssueDueWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -1260,7 +1287,7 @@
:tags: []
- :name: mail_scheduler:mail_scheduler_notification_service
:worker_name: MailScheduler::NotificationServiceWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
@@ -1737,7 +1764,7 @@
:tags: []
- :name: todos_destroyer:todos_destroyer_confidential_issue
:worker_name: TodosDestroyer::ConfidentialIssueWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -1746,7 +1773,7 @@
:tags: []
- :name: todos_destroyer:todos_destroyer_destroyed_designs
:worker_name: TodosDestroyer::DestroyedDesignsWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -1755,7 +1782,7 @@
:tags: []
- :name: todos_destroyer:todos_destroyer_destroyed_issuable
:worker_name: TodosDestroyer::DestroyedIssuableWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -1764,7 +1791,7 @@
:tags: []
- :name: todos_destroyer:todos_destroyer_entity_leave
:worker_name: TodosDestroyer::EntityLeaveWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -1773,7 +1800,7 @@
:tags: []
- :name: todos_destroyer:todos_destroyer_group_private
:worker_name: TodosDestroyer::GroupPrivateWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -1782,7 +1809,7 @@
:tags: []
- :name: todos_destroyer:todos_destroyer_private_features
:worker_name: TodosDestroyer::PrivateFeaturesWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -1791,7 +1818,7 @@
:tags: []
- :name: todos_destroyer:todos_destroyer_project_private
:worker_name: TodosDestroyer::ProjectPrivateWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -2052,7 +2079,7 @@
:tags: []
- :name: email_receiver
:worker_name: EmailReceiverWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
@@ -2116,7 +2143,7 @@
:tags: []
- :name: export_csv
:worker_name: ExportCsvWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
@@ -2206,13 +2233,22 @@
:tags: []
- :name: import_issues_csv
:worker_name: ImportIssuesCsvWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 2
:idempotent: true
:tags: []
+- :name: integrations_create_external_cross_reference
+ :worker_name: Integrations::CreateExternalCrossReferenceWorker
+ :feature_category: :integrations
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: invalid_gpg_signature_update
:worker_name: InvalidGpgSignatureUpdateWorker
:feature_category: :source_code_management
@@ -2233,7 +2269,7 @@
:tags: []
- :name: issuable_export_csv
:worker_name: IssuableExportCsvWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
@@ -2242,7 +2278,7 @@
:tags: []
- :name: issuable_label_links_destroy
:worker_name: Issuable::LabelLinksDestroyWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -2251,7 +2287,7 @@
:tags: []
- :name: issuables_clear_groups_issue_counter
:worker_name: Issuables::ClearGroupsIssueCounterWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -2260,7 +2296,7 @@
:tags: []
- :name: issue_placement
:worker_name: IssuePlacementWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
@@ -2269,7 +2305,25 @@
:tags: []
- :name: issue_rebalancing
:worker_name: IssueRebalancingWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
+- :name: issues_placement
+ :worker_name: Issues::PlacementWorker
+ :feature_category: :team_planning
+ :has_external_dependencies:
+ :urgency: :high
+ :resource_boundary: :cpu
+ :weight: 2
+ :idempotent: true
+ :tags: []
+- :name: issues_rebalancing
+ :worker_name: Issues::RebalancingWorker
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -2375,6 +2429,15 @@
:weight: 1
:idempotent:
:tags: []
+- :name: namespaces_invite_team_email
+ :worker_name: Namespaces::InviteTeamEmailWorker
+ :feature_category: :experimentation_activation
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
- :name: namespaces_onboarding_issue_created
:worker_name: Namespaces::OnboardingIssueCreatedWorker
:feature_category: :onboarding
@@ -2413,7 +2476,7 @@
:tags: []
- :name: new_issue
:worker_name: NewIssueWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
@@ -2431,7 +2494,7 @@
:tags: []
- :name: new_note
:worker_name: NewNoteWorker
- :feature_category: :issue_tracking
+ :feature_category: :team_planning
:has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
@@ -2799,6 +2862,15 @@
:weight: 1
:idempotent:
:tags: []
+- :name: tasks_to_be_done_create
+ :worker_name: TasksToBeDone::CreateWorker
+ :feature_category: :onboarding
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :cpu
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: update_external_pull_requests
:worker_name: UpdateExternalPullRequestsWorker
:feature_category: :source_code_management
diff --git a/app/workers/authorized_project_update/project_recalculate_worker.rb b/app/workers/authorized_project_update/project_recalculate_worker.rb
index 3d073f18622..1b5faee0b6f 100644
--- a/app/workers/authorized_project_update/project_recalculate_worker.rb
+++ b/app/workers/authorized_project_update/project_recalculate_worker.rb
@@ -7,6 +7,8 @@ module AuthorizedProjectUpdate
data_consistency :always
include Gitlab::ExclusiveLeaseHelpers
+ prepend WaitableWorker
+
feature_category :authentication_and_authorization
urgency :high
queue_namespace :authorized_project_update
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 46fe6c7f7ce..afe14369d43 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -25,11 +25,9 @@ class AuthorizedProjectsWorker
end
end
- # rubocop: disable CodeReuse/ActiveRecord
def perform(user_id)
- user = User.find_by(id: user_id)
+ user = User.find_by_id(user_id)
user&.refresh_authorized_projects(source: self.class.name)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
index a0d1d9dca45..78244e0941e 100644
--- a/app/workers/build_hooks_worker.rb
+++ b/app/workers/build_hooks_worker.rb
@@ -14,7 +14,7 @@ class BuildHooksWorker # rubocop:disable Scalability/IdempotentWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Ci::Build.includes({ runner: :tags })
- .find_by(id: build_id)
+ .find_by_id(build_id)
.try(:execute_hooks)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
index 4ab08bbd7fe..af1136de94c 100644
--- a/app/workers/build_queue_worker.rb
+++ b/app/workers/build_queue_worker.rb
@@ -12,11 +12,9 @@ class BuildQueueWorker # rubocop:disable Scalability/IdempotentWorker
worker_resource_boundary :cpu
data_consistency :sticky
- # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
- Ci::Build.find_by(id: build_id).try do |build|
+ Ci::Build.find_by_id(build_id).try do |build|
Ci::UpdateBuildQueueService.new.tick(build)
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index ce39ac946a9..114bced0b22 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -11,13 +11,11 @@ class BuildSuccessWorker # rubocop:disable Scalability/IdempotentWorker
queue_namespace :pipeline_processing
urgency :high
- # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
- Ci::Build.find_by(id: build_id).try do |build|
+ Ci::Build.find_by_id(build_id).try do |build|
stop_environment(build) if build.stops_environment?
end
end
- # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/workers/chat_notification_worker.rb b/app/workers/chat_notification_worker.rb
index 2a2e94cc6f1..23d8a1ec29d 100644
--- a/app/workers/chat_notification_worker.rb
+++ b/app/workers/chat_notification_worker.rb
@@ -16,9 +16,8 @@ class ChatNotificationWorker # rubocop:disable Scalability/IdempotentWorker
RESCHEDULE_INTERVAL = 2.seconds
RESCHEDULE_TIMEOUT = 5.minutes
- # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id, reschedule_count = 0)
- Ci::Build.find_by(id: build_id).try do |build|
+ Ci::Build.find_by_id(build_id).try do |build|
send_response(build)
end
rescue Gitlab::Chat::Output::MissingBuildSectionError
@@ -30,7 +29,6 @@ class ChatNotificationWorker # rubocop:disable Scalability/IdempotentWorker
# the job instead of producing an error.
self.class.perform_in(RESCHEDULE_INTERVAL, build_id, reschedule_count + 1)
end
- # rubocop: enable CodeReuse/ActiveRecord
def send_response(build)
Gitlab::Chat::Responder.responder_for(build).try do |responder|
diff --git a/app/workers/ci/archive_trace_worker.rb b/app/workers/ci/archive_trace_worker.rb
index 503cfc07c25..5a22a5c74ee 100644
--- a/app/workers/ci/archive_trace_worker.rb
+++ b/app/workers/ci/archive_trace_worker.rb
@@ -9,12 +9,10 @@ module Ci
sidekiq_options retry: 3
include PipelineBackgroundQueue
- # rubocop: disable CodeReuse/ActiveRecord
def perform(job_id)
- Ci::Build.without_archived_trace.find_by(id: job_id).try do |job|
+ Ci::Build.without_archived_trace.find_by_id(job_id).try do |job|
Ci::ArchiveTraceService.new.execute(job, worker_name: self.class.name)
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/workers/ci/build_finished_worker.rb b/app/workers/ci/build_finished_worker.rb
index f047ba8fde5..aa12bdb009e 100644
--- a/app/workers/ci/build_finished_worker.rb
+++ b/app/workers/ci/build_finished_worker.rb
@@ -16,7 +16,7 @@ module Ci
ARCHIVE_TRACES_IN = 2.minutes.freeze
def perform(build_id)
- return unless build = Ci::Build.find_by(id: build_id) # rubocop: disable CodeReuse/ActiveRecord
+ return unless build = Ci::Build.find_by_id(build_id)
return unless build.project
return if build.project.pending_delete?
diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb
index 79881ec84fd..62fc3efd4f0 100644
--- a/app/workers/ci/build_trace_chunk_flush_worker.rb
+++ b/app/workers/ci/build_trace_chunk_flush_worker.rb
@@ -13,12 +13,10 @@ module Ci
idempotent!
- # rubocop: disable CodeReuse/ActiveRecord
def perform(id)
- ::Ci::BuildTraceChunk.find_by(id: id).try do |chunk|
+ ::Ci::BuildTraceChunk.find_by_id(id).try do |chunk|
chunk.persist_data!
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb b/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb
index 160947fb38e..590514424bb 100644
--- a/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb
+++ b/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb
@@ -15,9 +15,12 @@ module Ci
::Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
break unless pipeline.has_archive_artifacts?
- ::Ci::UnlockArtifactsService
+ results = ::Ci::UnlockArtifactsService
.new(pipeline.project, pipeline.user)
.execute(pipeline.ci_ref, pipeline)
+
+ log_extra_metadata_on_done(:unlocked_pipelines, results[:unlocked_pipelines])
+ log_extra_metadata_on_done(:unlocked_job_artifacts, results[:unlocked_job_artifacts])
end
end
end
diff --git a/app/workers/ci/ref_delete_unlock_artifacts_worker.rb b/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
index 0c217644cc4..aeadf111bfb 100644
--- a/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
+++ b/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
@@ -15,9 +15,12 @@ module Ci
::Project.find_by_id(project_id).try do |project|
::User.find_by_id(user_id).try do |user|
project.ci_refs.find_by_ref_path(ref_path).try do |ci_ref|
- ::Ci::UnlockArtifactsService
+ results = ::Ci::UnlockArtifactsService
.new(project, user)
.execute(ci_ref)
+
+ log_extra_metadata_on_done(:unlocked_pipelines, results[:unlocked_pipelines])
+ log_extra_metadata_on_done(:unlocked_job_artifacts, results[:unlocked_job_artifacts])
end
end
end
diff --git a/app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb b/app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb
index 98b4f4ad73a..533cb7c425f 100644
--- a/app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb
+++ b/app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb
@@ -21,7 +21,7 @@ module Ci
# Therefore, we can deduplicate the sidekiq jobs until the on-going
# assignment process has been finished.
idempotent!
- deduplicate :until_executed
+ deduplicate :until_executed, if_deduplicated: :reschedule_once
def perform(resource_group_id)
::Ci::ResourceGroup.find_by_id(resource_group_id).try do |resource_group|
diff --git a/app/workers/cluster_update_app_worker.rb b/app/workers/cluster_update_app_worker.rb
index cd2b2e38ea4..97fdec02ba4 100644
--- a/app/workers/cluster_update_app_worker.rb
+++ b/app/workers/cluster_update_app_worker.rb
@@ -26,16 +26,14 @@ class ClusterUpdateAppWorker # rubocop:disable Scalability/IdempotentWorker
private
- # rubocop: disable CodeReuse/ActiveRecord
def execute(app_name, app_id, project_id, scheduled_time)
- project = Project.find_by(id: project_id)
+ project = Project.find_by_id(project_id)
return unless project
find_application(app_name, app_id) do |app|
update_prometheus(app, scheduled_time, project)
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def update_prometheus(app, scheduled_time, project)
return unless app.managed_prometheus?
diff --git a/app/workers/clusters/applications/check_prometheus_health_worker.rb b/app/workers/clusters/applications/check_prometheus_health_worker.rb
deleted file mode 100644
index 4f85c5c5b7a..00000000000
--- a/app/workers/clusters/applications/check_prometheus_health_worker.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class CheckPrometheusHealthWorker
- include ApplicationWorker
-
- data_consistency :always
-
- # rubocop:disable Scalability/CronWorkerContext
- # This worker does not perform work scoped to a context
- include CronjobQueue
- # rubocop:enable Scalability/CronWorkerContext
-
- queue_namespace :incident_management
- feature_category :incident_management
- urgency :low
-
- idempotent!
- worker_has_external_dependencies!
-
- def perform
- demo_project_ids = Gitlab::Monitor::DemoProjects.primary_keys
-
- clusters = Clusters::Cluster.with_application_prometheus
- .with_project_http_integrations(demo_project_ids)
-
- # Move to a seperate worker with scoped context if expanded to do work on customer projects
- clusters.each { |cluster| Clusters::Applications::PrometheusHealthCheckService.new(cluster).execute }
- end
- end
- end
-end
diff --git a/app/workers/clusters/integrations/check_prometheus_health_worker.rb b/app/workers/clusters/integrations/check_prometheus_health_worker.rb
new file mode 100644
index 00000000000..0c0d86e975c
--- /dev/null
+++ b/app/workers/clusters/integrations/check_prometheus_health_worker.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Integrations
+ class CheckPrometheusHealthWorker
+ include ApplicationWorker
+
+ data_consistency :always
+
+ # rubocop:disable Scalability/CronWorkerContext
+ # This worker does not perform work scoped to a context
+ include CronjobQueue
+ # rubocop:enable Scalability/CronWorkerContext
+
+ feature_category :incident_management
+ urgency :low
+
+ idempotent!
+ worker_has_external_dependencies!
+
+ def perform
+ demo_project_ids = Gitlab::Monitor::DemoProjects.primary_keys
+
+ clusters = Clusters::Cluster.with_integration_prometheus
+ .with_project_http_integrations(demo_project_ids)
+
+ # Move to a seperate worker with scoped context if expanded to do work on customer projects
+ clusters.each { |cluster| Clusters::Integrations::PrometheusHealthCheckService.new(cluster).execute }
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb
index 3399a4f9b57..03a0b5fae00 100644
--- a/app/workers/concerns/application_worker.rb
+++ b/app/workers/concerns/application_worker.rb
@@ -14,6 +14,7 @@ module ApplicationWorker
LOGGING_EXTRA_KEY = 'extra'
DEFAULT_DELAY_INTERVAL = 1
+ SAFE_PUSH_BULK_LIMIT = 1000
included do
set_queue
@@ -54,6 +55,12 @@ module ApplicationWorker
subclass.after_set_class_attribute { subclass.set_queue }
end
+ def with_status
+ status_from_class = self.sidekiq_options_hash['status_expiration']
+
+ set(status_expiration: status_from_class || Gitlab::SidekiqStatus::DEFAULT_EXPIRATION)
+ end
+
def generated_queue_name
Gitlab::SidekiqConfig::WorkerRouter.queue_name_from_worker_name(self)
end
@@ -130,29 +137,62 @@ module ApplicationWorker
end
end
+ def log_bulk_perform_async?
+ @log_bulk_perform_async
+ end
+
+ def log_bulk_perform_async!
+ @log_bulk_perform_async = true
+ end
+
def queue_size
Sidekiq::Queue.new(queue).size
end
def bulk_perform_async(args_list)
- Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
+ if log_bulk_perform_async?
+ Sidekiq.logger.info('class' => self.name, 'args_list' => args_list, 'args_list_count' => args_list.length, 'message' => 'Inserting multiple jobs')
+ end
+
+ do_push_bulk(args_list).tap do |job_ids|
+ if log_bulk_perform_async?
+ Sidekiq.logger.info('class' => self.name, 'jid_list' => job_ids, 'jid_list_count' => job_ids.length, 'message' => 'Completed JID insertion')
+ end
+ end
end
def bulk_perform_in(delay, args_list, batch_size: nil, batch_delay: nil)
now = Time.now.to_i
- schedule = now + delay.to_i
+ base_schedule_at = now + delay.to_i
- if schedule <= now
- raise ArgumentError, _('The schedule time must be in the future!')
+ if base_schedule_at <= now
+ raise ArgumentError, 'The schedule time must be in the future!'
end
+ schedule_at = base_schedule_at
+
if batch_size && batch_delay
- args_list.each_slice(batch_size.to_i).with_index do |args_batch, idx|
- batch_schedule = schedule + idx * batch_delay.to_i
- Sidekiq::Client.push_bulk('class' => self, 'args' => args_batch, 'at' => batch_schedule)
+ batch_size = batch_size.to_i
+ batch_delay = batch_delay.to_i
+
+ raise ArgumentError, 'batch_size should be greater than 0' unless batch_size > 0
+ raise ArgumentError, 'batch_delay should be greater than 0' unless batch_delay > 0
+
+ # build an array of schedules corresponding to each item in `args_list`
+ bulk_schedule_at = Array.new(args_list.size) do |index|
+ batch_number = index / batch_size
+ base_schedule_at + (batch_number * batch_delay)
+ end
+
+ schedule_at = bulk_schedule_at
+ end
+
+ if Feature.enabled?(:sidekiq_push_bulk_in_batches)
+ in_safe_limit_batches(args_list, schedule_at) do |args_batch, schedule_at_for_batch|
+ Sidekiq::Client.push_bulk('class' => self, 'args' => args_batch, 'at' => schedule_at_for_batch)
end
else
- Sidekiq::Client.push_bulk('class' => self, 'args' => args_list, 'at' => schedule)
+ Sidekiq::Client.push_bulk('class' => self, 'args' => args_list, 'at' => schedule_at)
end
end
@@ -161,5 +201,44 @@ module ApplicationWorker
def delay_interval
DEFAULT_DELAY_INTERVAL.seconds
end
+
+ private
+
+ def do_push_bulk(args_list)
+ if Feature.enabled?(:sidekiq_push_bulk_in_batches)
+ in_safe_limit_batches(args_list) do |args_batch, _|
+ Sidekiq::Client.push_bulk('class' => self, 'args' => args_batch)
+ end
+ else
+ Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
+ end
+ end
+
+ def in_safe_limit_batches(args_list, schedule_at = nil, safe_limit = SAFE_PUSH_BULK_LIMIT)
+ # `schedule_at` could be one of
+ # - nil.
+ # - a single Numeric that represents time, like `30.minutes.from_now.to_i`.
+ # - an array, where each element is a Numeric that reprsents time.
+ # - Each element in this array would correspond to the time at which
+ # - the job in `args_list` at the corresponding index needs to be scheduled.
+
+ # In the case where `schedule_at` is an array of Numeric, it needs to be sliced
+ # in the same manner as the `args_list`, with each slice containing `safe_limit`
+ # number of elements.
+ schedule_at = schedule_at.each_slice(safe_limit).to_a if schedule_at.is_a?(Array)
+
+ args_list.each_slice(safe_limit).with_index.flat_map do |args_batch, index|
+ schedule_at_for_batch = process_schedule_at_for_batch(schedule_at, index)
+
+ yield(args_batch, schedule_at_for_batch)
+ end
+ end
+
+ def process_schedule_at_for_batch(schedule_at, index)
+ return unless schedule_at
+ return schedule_at[index] if schedule_at.is_a?(Array) && schedule_at.all?(Array)
+
+ schedule_at
+ end
end
end
diff --git a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
index eb1af0869bd..0a43a0fc4d2 100644
--- a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
@@ -8,9 +8,8 @@ module Gitlab
# project_id - The ID of the GitLab project to import the note into.
# hash - A Hash containing the details of the GitHub object to import.
# notify_key - The Redis key to notify upon completion, if any.
- # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, hash, notify_key = nil)
- project = Project.find_by(id: project_id)
+ project = Project.find_by_id(project_id)
return notify_waiter(notify_key) unless project
@@ -25,7 +24,6 @@ module Gitlab
.perform_in(client.rate_limit_resets_in, project.id, hash, notify_key)
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def try_import(*args)
import(*args)
diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb
index d7b4578af63..225716f6bf3 100644
--- a/app/workers/concerns/gitlab/github_import/stage_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -33,13 +33,13 @@ module Gitlab
self.class.perform_in(client.rate_limit_resets_in, project.id)
end
- # rubocop: disable CodeReuse/ActiveRecord
def find_project(id)
# If the project has been marked as failed we want to bail out
# automatically.
- Project.joins_import_state.where(import_state: { status: :started }).find_by(id: id)
+ # rubocop: disable CodeReuse/ActiveRecord
+ Project.joins_import_state.where(import_state: { status: :started }).find_by_id(id)
+ # rubocop: enable CodeReuse/ActiveRecord
end
- # rubocop: enable CodeReuse/ActiveRecord
def abort_on_failure
false
diff --git a/app/workers/concerns/gitlab/jira_import/import_worker.rb b/app/workers/concerns/gitlab/jira_import/import_worker.rb
index 107b6e2e9be..d18b9ff023b 100644
--- a/app/workers/concerns/gitlab/jira_import/import_worker.rb
+++ b/app/workers/concerns/gitlab/jira_import/import_worker.rb
@@ -14,7 +14,7 @@ module Gitlab
end
def perform(project_id)
- project = Project.find_by(id: project_id) # rubocop: disable CodeReuse/ActiveRecord
+ project = Project.find_by_id(project_id)
return unless can_import?(project)
diff --git a/app/workers/concerns/limited_capacity/worker.rb b/app/workers/concerns/limited_capacity/worker.rb
index b4cdfda680f..bcedb4efcc0 100644
--- a/app/workers/concerns/limited_capacity/worker.rb
+++ b/app/workers/concerns/limited_capacity/worker.rb
@@ -47,7 +47,7 @@ module LimitedCapacity
# would be occupied by a job that will be performed in the distant future.
# We let the cron worker enqueue new jobs, this could be seen as our retry and
# back off mechanism because the job might fail again if executed immediately.
- sidekiq_options retry: 0
+ sidekiq_options retry: 0, status_expiration: Gitlab::SidekiqStatus::DEFAULT_EXPIRATION
deduplicate :none
end
diff --git a/app/workers/concerns/new_issuable.rb b/app/workers/concerns/new_issuable.rb
index 482a74f49f7..d761f023cad 100644
--- a/app/workers/concerns/new_issuable.rb
+++ b/app/workers/concerns/new_issuable.rb
@@ -10,21 +10,17 @@ module NewIssuable
user && issuable
end
- # rubocop: disable CodeReuse/ActiveRecord
def set_user(user_id)
- @user = User.find_by(id: user_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @user = User.find_by_id(user_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
log_error(User, user_id) unless @user # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def set_issuable(issuable_id)
- @issuable = issuable_class.find_by(id: issuable_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @issuable = issuable_class.find_by_id(issuable_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
log_error(issuable_class, issuable_id) unless @issuable # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
- # rubocop: enable CodeReuse/ActiveRecord
def log_error(record_class, record_id)
Gitlab::AppLogger.error("#{self.class}: couldn't find #{record_class} with ID=#{record_id}, skipping job")
diff --git a/app/workers/concerns/todos_destroyer_queue.rb b/app/workers/concerns/todos_destroyer_queue.rb
index 1bbccbfb1f9..1c31b64ad97 100644
--- a/app/workers/concerns/todos_destroyer_queue.rb
+++ b/app/workers/concerns/todos_destroyer_queue.rb
@@ -8,6 +8,6 @@ module TodosDestroyerQueue
included do
queue_namespace :todos_destroyer
- feature_category :issue_tracking
+ feature_category :team_planning
end
end
diff --git a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
index 69f5906f54c..7f7a77d0524 100644
--- a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
+++ b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
@@ -159,7 +159,10 @@ module ContainerExpirationPolicies
return unless tags_count && cached_tags_count && tags_count != 0
- log_extra_metadata_on_done(:cleanup_tags_service_cache_hit_ratio, cached_tags_count / tags_count.to_f)
+ ratio = cached_tags_count / tags_count.to_f
+ ratio_as_percentage = (ratio * 100).round(2)
+
+ log_extra_metadata_on_done(:cleanup_tags_service_cache_hit_ratio, ratio_as_percentage)
end
def log_truncate(result)
diff --git a/app/workers/create_commit_signature_worker.rb b/app/workers/create_commit_signature_worker.rb
index 01a2e109967..c35d2d3b33b 100644
--- a/app/workers/create_commit_signature_worker.rb
+++ b/app/workers/create_commit_signature_worker.rb
@@ -12,7 +12,6 @@ class CreateCommitSignatureWorker
idempotent!
loggable_arguments 0
- # rubocop: disable CodeReuse/ActiveRecord
def perform(commit_shas, project_id)
# Older versions of Git::BranchPushService may push a single commit ID on
# the stack. We need this to be backwards compatible.
@@ -20,7 +19,7 @@ class CreateCommitSignatureWorker
return if commit_shas.empty?
- project = Project.find_by(id: project_id)
+ project = Project.find_by_id(project_id)
return unless project
commits = project.commits_by(oids: commit_shas)
@@ -44,5 +43,4 @@ class CreateCommitSignatureWorker
Gitlab::AppLogger.error("Failed to create signature for commit #{commit.id}. Error: #{e.message}")
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/database/drop_detached_partitions_worker.rb b/app/workers/database/drop_detached_partitions_worker.rb
index 1e4dc20a0d2..126c46a338b 100644
--- a/app/workers/database/drop_detached_partitions_worker.rb
+++ b/app/workers/database/drop_detached_partitions_worker.rb
@@ -12,7 +12,7 @@ module Database
def perform
Gitlab::Database::Partitioning.drop_detached_partitions
ensure
- Gitlab::Database::Partitioning::PartitionMonitoring.new.report_metrics
+ Gitlab::Database::Partitioning.report_metrics
end
end
end
diff --git a/app/workers/database/partition_management_worker.rb b/app/workers/database/partition_management_worker.rb
index 5a1f139dc29..ba1ad7a0e81 100644
--- a/app/workers/database/partition_management_worker.rb
+++ b/app/workers/database/partition_management_worker.rb
@@ -14,7 +14,7 @@ module Database
def perform
Gitlab::Database::Partitioning.sync_partitions
ensure
- Gitlab::Database::Partitioning::PartitionMonitoring.new.report_metrics
+ Gitlab::Database::Partitioning.report_metrics
end
end
end
diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb
index beeca559060..a4d6adc2195 100644
--- a/app/workers/delete_container_repository_worker.rb
+++ b/app/workers/delete_container_repository_worker.rb
@@ -15,10 +15,9 @@ class DeleteContainerRepositoryWorker # rubocop:disable Scalability/IdempotentWo
attr_reader :container_repository
- # rubocop: disable CodeReuse/ActiveRecord
def perform(current_user_id, container_repository_id)
- current_user = User.find_by(id: current_user_id)
- @container_repository = ContainerRepository.find_by(id: container_repository_id)
+ current_user = User.find_by_id(current_user_id)
+ @container_repository = ContainerRepository.find_by_id(container_repository_id)
project = container_repository&.project
return unless current_user && container_repository && project
@@ -29,7 +28,6 @@ class DeleteContainerRepositoryWorker # rubocop:disable Scalability/IdempotentWo
Projects::ContainerRepository::DestroyService.new(project, current_user).execute(container_repository)
end
end
- # rubocop: enable CodeReuse/ActiveRecord
# For ExclusiveLeaseGuard concern
def lease_key
diff --git a/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb b/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb
index fed469e6dc8..6a1de00ce80 100644
--- a/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb
+++ b/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb
@@ -13,9 +13,8 @@ module DependencyProxy
def perform
DependencyProxy::ImageTtlGroupPolicy.enabled.each do |policy|
- # Technical Debt: change to read_before https://gitlab.com/gitlab-org/gitlab/-/issues/341536
- qualified_blobs = policy.group.dependency_proxy_blobs.active.updated_before(policy.ttl)
- qualified_manifests = policy.group.dependency_proxy_manifests.active.updated_before(policy.ttl)
+ qualified_blobs = policy.group.dependency_proxy_blobs.active.read_before(policy.ttl)
+ qualified_manifests = policy.group.dependency_proxy_manifests.active.read_before(policy.ttl)
enqueue_blob_cleanup_job if expire_artifacts(qualified_blobs, DependencyProxy::Blob)
enqueue_manifest_cleanup_job if expire_artifacts(qualified_manifests, DependencyProxy::Manifest)
diff --git a/app/workers/deployments/archive_in_project_worker.rb b/app/workers/deployments/archive_in_project_worker.rb
new file mode 100644
index 00000000000..2de4cacbbd6
--- /dev/null
+++ b/app/workers/deployments/archive_in_project_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Deployments
+ class ArchiveInProjectWorker
+ include ApplicationWorker
+
+ queue_namespace :deployment
+ feature_category :continuous_delivery
+ idempotent!
+ deduplicate :until_executed, including_scheduled: true
+ data_consistency :delayed
+
+ def perform(project_id)
+ Project.find_by_id(project_id).try do |project|
+ Deployments::ArchiveInProjectService.new(project, nil).execute
+ end
+ end
+ end
+end
diff --git a/app/workers/detect_repository_languages_worker.rb b/app/workers/detect_repository_languages_worker.rb
index 8c7ef6c9e32..702b83a3185 100644
--- a/app/workers/detect_repository_languages_worker.rb
+++ b/app/workers/detect_repository_languages_worker.rb
@@ -14,16 +14,14 @@ class DetectRepositoryLanguagesWorker # rubocop:disable Scalability/IdempotentWo
attr_reader :project
- # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, user_id = nil)
- @project = Project.find_by(id: project_id)
+ @project = Project.find_by_id(project_id)
return unless project
try_obtain_lease do
::Projects::DetectRepositoryLanguagesService.new(project).execute
end
end
- # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 51211834e06..321d492f0f3 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -7,7 +7,7 @@ class EmailReceiverWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
- feature_category :issue_tracking
+ feature_category :team_planning
urgency :high
weight 2
@@ -118,7 +118,9 @@ class EmailReceiverWorker # rubocop:disable Scalability/IdempotentWorker
end
if reason
- EmailRejectionMailer.rejection(reason, raw, can_retry).deliver_later
+ receiver.mail.body = nil
+
+ EmailRejectionMailer.rejection(reason, receiver.mail.encoded, can_retry).deliver_later
end
end
end
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 0230a226567..d7bd8207f06 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -87,13 +87,14 @@ class EmailsOnPushWorker # rubocop:disable Scalability/IdempotentWorker
private
def send_email(recipient, project_id, options)
- # Generating the body of this email can be expensive, so only do it once
- @skip_premailer ||= email.present?
- @email ||= Notify.repository_push_email(project_id, options)
-
- email.to = recipient
- email.add_message_id
- email.header[:skip_premailer] = true if skip_premailer
- email.deliver_now
+ @email ||= Notify.repository_push_email(project_id, options).tap do |mail|
+ Premailer::Rails::Hook.perform(mail)
+ end
+
+ current_email = email.dup
+ current_email.to = recipient
+ current_email.add_message_id
+ current_email.header[:skip_premailer] = true
+ current_email.deliver_now
end
end
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
index 77b8f59e365..948e1a59b07 100644
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -9,17 +9,17 @@ class ExpireBuildInstanceArtifactsWorker # rubocop:disable Scalability/Idempoten
feature_category :build_artifacts
- # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
+ # rubocop: disable CodeReuse/ActiveRecord
build = Ci::Build
.with_expired_artifacts
.reorder(nil)
- .find_by(id: build_id)
+ .find_by_id(build_id)
+ # rubocop: enable CodeReuse/ActiveRecord
return unless build&.project && !build.project.pending_delete
Gitlab::AppLogger.info("Removing artifacts for build #{build.id}...")
build.erase_erasable_artifacts!
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
index 7374f650546..3c5a7717d70 100644
--- a/app/workers/expire_job_cache_worker.rb
+++ b/app/workers/expire_job_cache_worker.rb
@@ -14,9 +14,8 @@ class ExpireJobCacheWorker # rubocop:disable Scalability/IdempotentWorker
deduplicate :until_executing, including_scheduled: true
idempotent!
- # rubocop: disable CodeReuse/ActiveRecord
def perform(job_id)
- job = CommitStatus.preload(:pipeline, :project).find_by(id: job_id)
+ job = CommitStatus.preload(:pipeline, :project).find_by_id(job_id) # rubocop: disable CodeReuse/ActiveRecord
return unless job
pipeline = job.pipeline
@@ -25,7 +24,6 @@ class ExpireJobCacheWorker # rubocop:disable Scalability/IdempotentWorker
Gitlab::EtagCaching::Store.new.touch(project_job_path(project, job))
ExpirePipelineCacheWorker.perform_async(pipeline.id)
end
- # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
index 07e6939d1c7..9a0c617da57 100644
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -17,13 +17,11 @@ class ExpirePipelineCacheWorker
# Uncomment once https://gitlab.com/gitlab-org/gitlab/-/issues/325291 is resolved
# idempotent!
- # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
- pipeline = Ci::Pipeline.find_by(id: pipeline_id)
+ pipeline = Ci::Pipeline.find_by_id(pipeline_id)
return unless pipeline
Ci::ExpirePipelineCacheService.new.execute(pipeline)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
# rubocop:enable Scalability/IdempotentWorker
diff --git a/app/workers/export_csv_worker.rb b/app/workers/export_csv_worker.rb
index 68feaa61cdd..c5feb02b2bd 100644
--- a/app/workers/export_csv_worker.rb
+++ b/app/workers/export_csv_worker.rb
@@ -7,7 +7,7 @@ class ExportCsvWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
- feature_category :issue_tracking
+ feature_category :team_planning
worker_resource_boundary :cpu
loggable_arguments 2
diff --git a/app/workers/gitlab/jira_import/import_issue_worker.rb b/app/workers/gitlab/jira_import/import_issue_worker.rb
index eabe7328b92..3824cc1f3ef 100644
--- a/app/workers/gitlab/jira_import/import_issue_worker.rb
+++ b/app/workers/gitlab/jira_import/import_issue_worker.rb
@@ -54,7 +54,7 @@ module Gitlab
label_link_attrs << build_label_attrs(issue_id, import_label_id.to_i)
- Gitlab::Database.main.bulk_insert(LabelLink.table_name, label_link_attrs) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(LabelLink.table_name, label_link_attrs) # rubocop:disable Gitlab/BulkInsert
end
def assign_issue(project_id, issue_id, assignee_ids)
@@ -62,7 +62,7 @@ module Gitlab
assignee_attrs = assignee_ids.map { |user_id| { issue_id: issue_id, user_id: user_id } }
- Gitlab::Database.main.bulk_insert(IssueAssignee.table_name, assignee_attrs) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(IssueAssignee.table_name, assignee_attrs) # rubocop:disable Gitlab/BulkInsert
end
def build_label_attrs(issue_id, label_id)
diff --git a/app/workers/gitlab/jira_import/stage/start_import_worker.rb b/app/workers/gitlab/jira_import/stage/start_import_worker.rb
index e0de3ee169e..3f6ad66e278 100644
--- a/app/workers/gitlab/jira_import/stage/start_import_worker.rb
+++ b/app/workers/gitlab/jira_import/stage/start_import_worker.rb
@@ -16,7 +16,7 @@ module Gitlab
attr_reader :project
def perform(project_id)
- @project = Project.find_by(id: project_id) # rubocop: disable CodeReuse/ActiveRecord
+ @project = Project.find_by_id(project_id)
return unless start_import
diff --git a/app/workers/hashed_storage/project_migrate_worker.rb b/app/workers/hashed_storage/project_migrate_worker.rb
index bcc80cc2a70..460aac3f2f2 100644
--- a/app/workers/hashed_storage/project_migrate_worker.rb
+++ b/app/workers/hashed_storage/project_migrate_worker.rb
@@ -16,12 +16,11 @@ module HashedStorage
attr_reader :project_id
- # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, old_disk_path = nil)
@project_id = project_id # we need to set this in order to create the lease_key
try_obtain_lease do
- project = Project.without_deleted.find_by(id: project_id)
+ project = Project.without_deleted.find_by_id(project_id)
break unless project && project.storage_upgradable?
old_disk_path ||= Storage::LegacyProject.new(project).disk_path
@@ -29,6 +28,5 @@ module HashedStorage
::Projects::HashedStorage::MigrationService.new(project, old_disk_path, logger: logger).execute
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/workers/hashed_storage/project_rollback_worker.rb b/app/workers/hashed_storage/project_rollback_worker.rb
index 07a7ab63718..91ea3dd9189 100644
--- a/app/workers/hashed_storage/project_rollback_worker.rb
+++ b/app/workers/hashed_storage/project_rollback_worker.rb
@@ -16,12 +16,11 @@ module HashedStorage
attr_reader :project_id
- # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, old_disk_path = nil)
@project_id = project_id # we need to set this in order to create the lease_key
try_obtain_lease do
- project = Project.without_deleted.find_by(id: project_id)
+ project = Project.without_deleted.find_by_id(project_id)
break unless project
old_disk_path ||= project.disk_path
@@ -29,6 +28,5 @@ module HashedStorage
::Projects::HashedStorage::RollbackService.new(project, old_disk_path, logger: logger).execute
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/workers/import_issues_csv_worker.rb b/app/workers/import_issues_csv_worker.rb
index 58e411c7b19..fe5b1c13d56 100644
--- a/app/workers/import_issues_csv_worker.rb
+++ b/app/workers/import_issues_csv_worker.rb
@@ -8,7 +8,7 @@ class ImportIssuesCsvWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
idempotent!
- feature_category :issue_tracking
+ feature_category :team_planning
worker_resource_boundary :cpu
weight 2
diff --git a/app/workers/integrations/create_external_cross_reference_worker.rb b/app/workers/integrations/create_external_cross_reference_worker.rb
new file mode 100644
index 00000000000..02c1315249e
--- /dev/null
+++ b/app/workers/integrations/create_external_cross_reference_worker.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Integrations
+ class CreateExternalCrossReferenceWorker
+ include ApplicationWorker
+
+ data_consistency :delayed
+
+ feature_category :integrations
+ urgency :low
+ idempotent!
+ deduplicate :until_executed, including_scheduled: true
+ loggable_arguments 2
+
+ def perform(project_id, external_issue_id, mentionable_type, mentionable_id, author_id)
+ project = Project.find_by_id(project_id) || return
+ author = User.find_by_id(author_id) || return
+ mentionable = find_mentionable(mentionable_type, mentionable_id, project) || return
+ external_issue = ExternalIssue.new(external_issue_id, project)
+
+ project.external_issue_tracker.create_cross_reference_note(
+ external_issue,
+ mentionable,
+ author
+ )
+ end
+
+ private
+
+ def find_mentionable(mentionable_type, mentionable_id, project)
+ mentionable_class = mentionable_type.safe_constantize
+
+ # Passing an invalid mentionable_class is a developer error, so we don't want to retry the job
+ # but still track the exception on production, and raise it in development.
+ unless mentionable_class && mentionable_class < Mentionable
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new("Unexpected class '#{mentionable_type}' is not a Mentionable"))
+ return
+ end
+
+ if mentionable_type == 'Commit'
+ project.commit(mentionable_id)
+ else
+ mentionable_class.find_by_id(mentionable_id)
+ end
+ end
+ end
+end
diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb
index c9567e102d3..7dc1e395597 100644
--- a/app/workers/invalid_gpg_signature_update_worker.rb
+++ b/app/workers/invalid_gpg_signature_update_worker.rb
@@ -10,13 +10,11 @@ class InvalidGpgSignatureUpdateWorker # rubocop:disable Scalability/IdempotentWo
feature_category :source_code_management
weight 2
- # rubocop: disable CodeReuse/ActiveRecord
def perform(gpg_key_id)
- gpg_key = GpgKey.find_by(id: gpg_key_id)
+ gpg_key = GpgKey.find_by_id(gpg_key_id)
return unless gpg_key
Gitlab::Gpg::InvalidGpgSignatureUpdater.new(gpg_key).run
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/issuable/label_links_destroy_worker.rb b/app/workers/issuable/label_links_destroy_worker.rb
index f88c061bafb..da785550a43 100644
--- a/app/workers/issuable/label_links_destroy_worker.rb
+++ b/app/workers/issuable/label_links_destroy_worker.rb
@@ -7,7 +7,7 @@ module Issuable
data_consistency :always
idempotent!
- feature_category :issue_tracking
+ feature_category :team_planning
def perform(target_id, target_type)
::Issuable::DestroyLabelLinksService.new(target_id, target_type).execute
diff --git a/app/workers/issuable_export_csv_worker.rb b/app/workers/issuable_export_csv_worker.rb
index 7e2c3407772..9d543a21dc3 100644
--- a/app/workers/issuable_export_csv_worker.rb
+++ b/app/workers/issuable_export_csv_worker.rb
@@ -7,7 +7,7 @@ class IssuableExportCsvWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
- feature_category :issue_tracking
+ feature_category :team_planning
worker_resource_boundary :cpu
loggable_arguments 2
diff --git a/app/workers/issuables/clear_groups_issue_counter_worker.rb b/app/workers/issuables/clear_groups_issue_counter_worker.rb
index 9e62224b83d..82026bc21c7 100644
--- a/app/workers/issuables/clear_groups_issue_counter_worker.rb
+++ b/app/workers/issuables/clear_groups_issue_counter_worker.rb
@@ -8,7 +8,7 @@ module Issuables
idempotent!
urgency :low
- feature_category :issue_tracking
+ feature_category :team_planning
def perform(group_ids = [])
return if group_ids.empty?
diff --git a/app/workers/issue_due_scheduler_worker.rb b/app/workers/issue_due_scheduler_worker.rb
index ad3470ae64b..ab586dce717 100644
--- a/app/workers/issue_due_scheduler_worker.rb
+++ b/app/workers/issue_due_scheduler_worker.rb
@@ -7,7 +7,7 @@ class IssueDueSchedulerWorker # rubocop:disable Scalability/IdempotentWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
- feature_category :issue_tracking
+ feature_category :team_planning
# rubocop: disable CodeReuse/ActiveRecord
def perform
diff --git a/app/workers/issue_placement_worker.rb b/app/workers/issue_placement_worker.rb
index 22e2a8e95f4..cfd72b90a42 100644
--- a/app/workers/issue_placement_worker.rb
+++ b/app/workers/issue_placement_worker.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+# todo: remove this worker and it's queue definition from all_queues after Issues::PlacementWorker is deployed
+# We want to keep it for one release in case some jobs are already scheduled in the old queue so we need the worker
+# to be available to finish those. All new jobs will be queued into the new queue.
class IssuePlacementWorker
include ApplicationWorker
@@ -9,7 +12,7 @@ class IssuePlacementWorker
idempotent!
deduplicate :until_executed, including_scheduled: true
- feature_category :issue_tracking
+ feature_category :team_planning
urgency :high
worker_resource_boundary :cpu
weight 2
diff --git a/app/workers/issue_rebalancing_worker.rb b/app/workers/issue_rebalancing_worker.rb
index 01984197aae..a43e76feae4 100644
--- a/app/workers/issue_rebalancing_worker.rb
+++ b/app/workers/issue_rebalancing_worker.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+# todo: remove this worker and it's queue definition from all_queues after Issue::RebalancingWorker is released.
+# We want to keep it for one release in case some jobs are already scheduled in the old queue so we need the worker
+# to be available to finish those. All new jobs will be queued into the new queue.
class IssueRebalancingWorker
include ApplicationWorker
@@ -9,13 +12,14 @@ class IssueRebalancingWorker
idempotent!
urgency :low
- feature_category :issue_tracking
+ feature_category :team_planning
deduplicate :until_executed, including_scheduled: true
def perform(ignore = nil, project_id = nil, root_namespace_id = nil)
# we need to have exactly one of the project_id and root_namespace_id params be non-nil
raise ArgumentError, "Expected only one of the params project_id: #{project_id} and root_namespace_id: #{root_namespace_id}" if project_id && root_namespace_id
return if project_id.nil? && root_namespace_id.nil?
+ return if ::Gitlab::Issues::Rebalancing::State.rebalance_recently_finished?(project_id, root_namespace_id)
# pull the projects collection to be rebalanced either the project if namespace is not a group(i.e. user namesapce)
# or the root namespace, this also makes the worker backward compatible with previous version where a project_id was
diff --git a/app/workers/issues/placement_worker.rb b/app/workers/issues/placement_worker.rb
new file mode 100644
index 00000000000..ec29a754128
--- /dev/null
+++ b/app/workers/issues/placement_worker.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Issues
+ class PlacementWorker
+ include ApplicationWorker
+
+ data_consistency :always
+
+ sidekiq_options retry: 3
+
+ idempotent!
+ deduplicate :until_executed, including_scheduled: true
+ feature_category :team_planning
+ urgency :high
+ worker_resource_boundary :cpu
+ weight 2
+
+ # Move at most the most recent 100 issues
+ QUERY_LIMIT = 100
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(issue_id, project_id = nil)
+ issue = find_issue(issue_id, project_id)
+ return unless issue
+
+ # Temporary disable moving null elements because of performance problems
+ # For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
+ return if issue.blocked_for_repositioning?
+
+ # Move the oldest 100 unpositioned items to the end.
+ # This is to deal with out-of-order execution of the worker,
+ # while preserving creation order.
+ to_place = Issue
+ .relative_positioning_query_base(issue)
+ .with_null_relative_position
+ .order({ created_at: :asc }, { id: :asc })
+ .limit(QUERY_LIMIT + 1)
+ .to_a
+
+ leftover = to_place.pop if to_place.count > QUERY_LIMIT
+
+ Issue.move_nulls_to_end(to_place)
+ Issues::BaseService.new(project: nil).rebalance_if_needed(to_place.max_by(&:relative_position))
+ Issues::PlacementWorker.perform_async(nil, leftover.project_id) if leftover.present?
+ rescue RelativePositioning::NoSpaceLeft => e
+ Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id, project_id: project_id)
+ Issues::RebalancingWorker.perform_async(nil, *root_namespace_id_to_rebalance(issue, project_id))
+ end
+
+ def find_issue(issue_id, project_id)
+ return Issue.id_in(issue_id).take if issue_id
+
+ project = Project.id_in(project_id).take
+ return unless project
+
+ project.issues.take
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def root_namespace_id_to_rebalance(issue, project_id)
+ project_id = project_id.presence || issue.project_id
+ Project.find(project_id)&.self_or_root_group_ids
+ end
+ end
+end
diff --git a/app/workers/issues/rebalancing_worker.rb b/app/workers/issues/rebalancing_worker.rb
new file mode 100644
index 00000000000..466617d9fa1
--- /dev/null
+++ b/app/workers/issues/rebalancing_worker.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Issues
+ class RebalancingWorker
+ include ApplicationWorker
+
+ data_consistency :always
+
+ sidekiq_options retry: 3
+
+ idempotent!
+ urgency :low
+ feature_category :team_planning
+ deduplicate :until_executed, including_scheduled: true
+
+ def perform(ignore = nil, project_id = nil, root_namespace_id = nil)
+ # we need to have exactly one of the project_id and root_namespace_id params be non-nil
+ raise ArgumentError, "Expected only one of the params project_id: #{project_id} and root_namespace_id: #{root_namespace_id}" if project_id && root_namespace_id
+ return if project_id.nil? && root_namespace_id.nil?
+
+ # pull the projects collection to be rebalanced either the project if namespace is not a group(i.e. user namesapce)
+ # or the root namespace, this also makes the worker backward compatible with previous version where a project_id was
+ # passed as the param
+ projects_to_rebalance = projects_collection(project_id, root_namespace_id)
+
+ # something might have happened with the namespace between scheduling the worker and actually running it,
+ # maybe it was removed.
+ if projects_to_rebalance.blank?
+ Gitlab::ErrorTracking.log_exception(
+ ArgumentError.new("Projects to be rebalanced not found for arguments: project_id #{project_id}, root_namespace_id: #{root_namespace_id}"),
+ { project_id: project_id, root_namespace_id: root_namespace_id })
+
+ return
+ end
+
+ Issues::RelativePositionRebalancingService.new(projects_to_rebalance).execute
+ rescue Issues::RelativePositionRebalancingService::TooManyConcurrentRebalances => e
+ Gitlab::ErrorTracking.log_exception(e, root_namespace_id: root_namespace_id, project_id: project_id)
+ end
+
+ private
+
+ def projects_collection(project_id, root_namespace_id)
+ # we can have either project_id(older version) or project_id if project is part of a user namespace and not a group
+ # or root_namespace_id(newer version) never both.
+ return Project.id_in([project_id]) if project_id
+
+ Namespace.find_by_id(root_namespace_id)&.all_projects
+ end
+ end
+end
diff --git a/app/workers/issues/reschedule_stuck_issue_rebalances_worker.rb b/app/workers/issues/reschedule_stuck_issue_rebalances_worker.rb
new file mode 100644
index 00000000000..d1759589cc0
--- /dev/null
+++ b/app/workers/issues/reschedule_stuck_issue_rebalances_worker.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Issues
+ class RescheduleStuckIssueRebalancesWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ data_consistency :sticky
+
+ idempotent!
+ urgency :low
+ feature_category :team_planning
+ deduplicate :until_executed, including_scheduled: true
+
+ def perform
+ namespace_ids, project_ids = ::Gitlab::Issues::Rebalancing::State.fetch_rebalancing_groups_and_projects
+
+ return if namespace_ids.blank? && project_ids.blank?
+
+ namespaces = Namespace.id_in(namespace_ids)
+ projects = Project.id_in(project_ids)
+
+ IssueRebalancingWorker.bulk_perform_async_with_contexts(
+ namespaces,
+ arguments_proc: -> (namespace) { [nil, nil, namespace.id] },
+ context_proc: -> (namespace) { { namespace: namespace } }
+ )
+
+ IssueRebalancingWorker.bulk_perform_async_with_contexts(
+ projects,
+ arguments_proc: -> (project) { [nil, project.id, nil] },
+ context_proc: -> (project) { { project: project } }
+ )
+ end
+ end
+end
diff --git a/app/workers/loose_foreign_keys/cleanup_worker.rb b/app/workers/loose_foreign_keys/cleanup_worker.rb
new file mode 100644
index 00000000000..b4565dbf624
--- /dev/null
+++ b/app/workers/loose_foreign_keys/cleanup_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module LooseForeignKeys
+ class CleanupWorker
+ include ApplicationWorker
+ include Gitlab::ExclusiveLeaseHelpers
+ include CronjobQueue # rubocop: disable Scalability/CronWorkerContext
+
+ feature_category :sharding
+ data_consistency :always
+ idempotent!
+
+ def perform
+ return if Feature.disabled?(:loose_foreign_key_cleanup, default_enabled: :yaml)
+
+ ttl = ModificationTracker::MAX_RUNTIME + 1.minute
+ in_lock(self.class.name.underscore, ttl: ttl, retries: 0) do
+ # TODO: Iterate over the connections
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/341513
+ stats = ProcessDeletedRecordsService.new(connection: ApplicationRecord.connection).execute
+ log_extra_metadata_on_done(:stats, stats)
+ end
+ end
+ end
+end
diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb
index 4c17f8df722..585fa43916e 100644
--- a/app/workers/mail_scheduler/issue_due_worker.rb
+++ b/app/workers/mail_scheduler/issue_due_worker.rb
@@ -9,7 +9,7 @@ module MailScheduler
sidekiq_options retry: 3
include MailSchedulerQueue
- feature_category :issue_tracking
+ feature_category :team_planning
# rubocop: disable CodeReuse/ActiveRecord
def perform(project_id)
diff --git a/app/workers/mail_scheduler/notification_service_worker.rb b/app/workers/mail_scheduler/notification_service_worker.rb
index 0e6494a45d6..25c9ac5547b 100644
--- a/app/workers/mail_scheduler/notification_service_worker.rb
+++ b/app/workers/mail_scheduler/notification_service_worker.rb
@@ -11,7 +11,7 @@ module MailScheduler
sidekiq_options retry: 3
include MailSchedulerQueue
- feature_category :issue_tracking
+ feature_category :team_planning
worker_resource_boundary :cpu
loggable_arguments 0
diff --git a/app/workers/namespaces/invite_team_email_worker.rb b/app/workers/namespaces/invite_team_email_worker.rb
new file mode 100644
index 00000000000..eabf33a7fba
--- /dev/null
+++ b/app/workers/namespaces/invite_team_email_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Namespaces
+ class InviteTeamEmailWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ data_consistency :always
+
+ feature_category :experimentation_activation
+ urgency :low
+
+ def perform(group_id, user_id)
+ # rubocop: disable CodeReuse/ActiveRecord
+ user = User.find_by(id: user_id)
+ group = Group.find_by(id: group_id)
+ # rubocop: enable CodeReuse/ActiveRecord
+ return unless user && group
+
+ Namespaces::InviteTeamEmailService.send_email(user, group)
+ end
+ end
+end
diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb
index 899545fc02c..13936fac1e4 100644
--- a/app/workers/new_issue_worker.rb
+++ b/app/workers/new_issue_worker.rb
@@ -8,7 +8,7 @@ class NewIssueWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
include NewIssuable
- feature_category :issue_tracking
+ feature_category :team_planning
urgency :high
worker_resource_boundary :cpu
weight 2
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index e54d84e382e..ecc78236161 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -7,21 +7,19 @@ class NewNoteWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
- feature_category :issue_tracking
+ feature_category :team_planning
urgency :high
worker_resource_boundary :cpu
weight 2
# Keep extra parameter to preserve backwards compatibility with
# old `NewNoteWorker` jobs (can remove later)
- # rubocop: disable CodeReuse/ActiveRecord
def perform(note_id, _params = {})
- if note = Note.find_by(id: note_id)
+ if note = Note.find_by_id(note_id)
NotificationService.new.new_note(note) unless note.skip_notification?
Notes::PostProcessService.new(note).execute
else
Gitlab::AppLogger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/pages_domain_verification_worker.rb b/app/workers/pages_domain_verification_worker.rb
index 59de00d40d5..1696abd7b70 100644
--- a/app/workers/pages_domain_verification_worker.rb
+++ b/app/workers/pages_domain_verification_worker.rb
@@ -9,15 +9,13 @@ class PagesDomainVerificationWorker # rubocop:disable Scalability/IdempotentWork
feature_category :pages
- # rubocop: disable CodeReuse/ActiveRecord
def perform(domain_id)
return if Gitlab::Database.read_only?
- domain = PagesDomain.find_by(id: domain_id)
+ domain = PagesDomain.find_by_id(domain_id)
return unless domain
VerifyPagesDomainService.new(domain).execute
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index 5e951ab2c3a..cceafbc7d2d 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -14,15 +14,13 @@ class PagesWorker # rubocop:disable Scalability/IdempotentWorker
send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
end
- # rubocop: disable CodeReuse/ActiveRecord
def deploy(build_id)
- build = Ci::Build.find_by(id: build_id)
+ build = Ci::Build.find_by_id(build_id)
update_contents = Projects::UpdatePagesService.new(build.project, build).execute
if update_contents[:status] == :success
Projects::UpdatePagesConfigurationService.new(build.project).execute
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def remove(namespace_path, project_path)
full_path = File.join(Settings.pages.path, namespace_path, project_path)
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index c67f3860a50..eb5d0086592 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -10,12 +10,10 @@ class PipelineHooksWorker # rubocop:disable Scalability/IdempotentWorker
worker_resource_boundary :cpu
data_consistency :delayed
- # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
- pipeline = Ci::Pipeline.find_by(id: pipeline_id)
+ pipeline = Ci::Pipeline.find_by_id(pipeline_id)
return unless pipeline
Ci::Pipelines::HookService.new(pipeline).execute
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index c2580c3d48b..4e98c7268ac 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -10,14 +10,12 @@ class PipelineMetricsWorker # rubocop:disable Scalability/IdempotentWorker
urgency :high
- # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
- Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
+ Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
update_metrics_for_active_pipeline(pipeline) if pipeline.active?
update_metrics_for_succeeded_pipeline(pipeline) if pipeline.success?
end
end
- # rubocop: enable CodeReuse/ActiveRecord
private
@@ -29,11 +27,9 @@ class PipelineMetricsWorker # rubocop:disable Scalability/IdempotentWorker
metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at, pipeline_id: pipeline.id)
end
- # rubocop: disable CodeReuse/ActiveRecord
def metrics(pipeline)
- MergeRequest::Metrics.where(merge_request_id: merge_requests(pipeline))
+ MergeRequest::Metrics.where(merge_request_id: merge_requests(pipeline)) # rubocop: disable CodeReuse/ActiveRecord
end
- # rubocop: enable CodeReuse/ActiveRecord
def merge_requests(pipeline)
pipeline.merge_requests_as_head_pipeline.map(&:id)
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index 9370b361068..b4712aaeafb 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -16,13 +16,11 @@ class PipelineProcessWorker
idempotent!
deduplicate :until_executing
- # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
- Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
+ Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
Ci::ProcessPipelineService
.new(pipeline)
.execute
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 7d0322361b8..45af15216fc 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -128,7 +128,6 @@ class PostReceive
end
def after_project_changes_hooks(project, user, refs, changes)
- experiment(:empty_repo_upload, project: project).track_initial_write
repository_update_hook_data = Gitlab::DataBuilder::Repository.update(project, user, changes, refs)
SystemHooksService.new.execute_hooks(repository_update_hook_data, :repository_update_hooks)
Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes)
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index 328fdc4717c..0e90b41e28d 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -23,9 +23,8 @@ class ProjectCacheWorker
# refresh, if empty all columns will be refreshed
# refresh_statistics - A boolean that determines whether project statistics should
# be updated.
- # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, files = [], statistics = [], refresh_statistics = true)
- project = Project.find_by(id: project_id)
+ project = Project.find_by_id(project_id)
return unless project
@@ -37,7 +36,6 @@ class ProjectCacheWorker
project.cleanup
end
- # rubocop: enable CodeReuse/ActiveRecord
# NOTE: triggering both an immediate update and one in 15 minutes if we
# successfully obtain the lease. That way, we only need to wait for the
diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb
index 6b8382ae433..908f867279f 100644
--- a/app/workers/propagate_service_template_worker.rb
+++ b/app/workers/propagate_service_template_worker.rb
@@ -13,13 +13,11 @@ class PropagateServiceTemplateWorker # rubocop:disable Scalability/IdempotentWor
LEASE_TIMEOUT = 4.hours.to_i
- # rubocop: disable CodeReuse/ActiveRecord
def perform(template_id)
return unless try_obtain_lease_for(template_id)
- Admin::PropagateServiceTemplate.propagate(Integration.find_by(id: template_id))
+ Admin::PropagateServiceTemplate.propagate(Integration.find_by_id(template_id))
end
- # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index 12042ebc4f0..f08d8231e43 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -11,16 +11,14 @@ class RunPipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
queue_namespace :pipeline_creation
feature_category :continuous_integration
- # rubocop: disable CodeReuse/ActiveRecord
def perform(schedule_id, user_id)
- schedule = Ci::PipelineSchedule.find_by(id: schedule_id)
- user = User.find_by(id: user_id)
+ schedule = Ci::PipelineSchedule.find_by_id(schedule_id)
+ user = User.find_by_id(user_id)
return unless schedule && user
run_pipeline_schedule(schedule, user)
end
- # rubocop: enable CodeReuse/ActiveRecord
def run_pipeline_schedule(schedule, user)
Ci::CreatePipelineService.new(schedule.project,
diff --git a/app/workers/ssh_keys/expired_notification_worker.rb b/app/workers/ssh_keys/expired_notification_worker.rb
index d8553b5a9a2..dc1efce51ce 100644
--- a/app/workers/ssh_keys/expired_notification_worker.rb
+++ b/app/workers/ssh_keys/expired_notification_worker.rb
@@ -29,7 +29,7 @@ module SshKeys
)
])
- scope = Key.expired_and_not_notified.order(order)
+ scope = Key.expired_today_and_not_notified.order(order)
iterator = Gitlab::Pagination::Keyset::Iterator.new(scope: scope, use_union_optimization: true)
iterator.each_batch(of: BATCH_SIZE) do |relation|
@@ -37,7 +37,7 @@ module SshKeys
users.each do |user|
with_context(user: user) do
- Keys::ExpiryNotificationService.new(user, { keys: user.expired_and_unnotified_keys, expiring_soon: false }).execute
+ Keys::ExpiryNotificationService.new(user, { keys: user.expired_today_and_unnotified_keys, expiring_soon: false }).execute
end
end
end
diff --git a/app/workers/tasks_to_be_done/create_worker.rb b/app/workers/tasks_to_be_done/create_worker.rb
new file mode 100644
index 00000000000..0953f190fd0
--- /dev/null
+++ b/app/workers/tasks_to_be_done/create_worker.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module TasksToBeDone
+ class CreateWorker
+ include ApplicationWorker
+
+ data_consistency :always
+ idempotent!
+ feature_category :onboarding
+ urgency :low
+ worker_resource_boundary :cpu
+
+ def perform(member_task_id, current_user_id, assignee_ids = [])
+ member_task = MemberTask.find(member_task_id)
+ current_user = User.find(current_user_id)
+ project = member_task.project
+
+ member_task.tasks_to_be_done.each do |task|
+ service_class(task)
+ .new(project: project, current_user: current_user, assignee_ids: assignee_ids)
+ .execute
+ end
+ end
+
+ private
+
+ def service_class(task)
+ "TasksToBeDone::Create#{task.to_s.camelize}TaskService".constantize
+ end
+ end
+end
diff --git a/app/workers/update_highest_role_worker.rb b/app/workers/update_highest_role_worker.rb
index d5df46c172b..064b8203d4d 100644
--- a/app/workers/update_highest_role_worker.rb
+++ b/app/workers/update_highest_role_worker.rb
@@ -13,17 +13,15 @@ class UpdateHighestRoleWorker
idempotent!
- # rubocop: disable CodeReuse/ActiveRecord
def perform(user_id)
- user = User.find_by(id: user_id)
+ user = User.find_by_id(user_id)
return unless user.present?
if user.active? && user.human? && !user.internal?
Users::UpdateHighestMemberRoleService.new(user).execute
else
- UserHighestRole.where(user_id: user_id).delete_all
+ UserHighestRole.where(user_id: user_id).delete_all # rubocop: disable CodeReuse/ActiveRecord
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 421a6e47425..5c96257cb63 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -13,15 +13,13 @@ class UpdateMergeRequestsWorker # rubocop:disable Scalability/IdempotentWorker
weight 3
loggable_arguments 2, 3, 4
- # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, user_id, oldrev, newrev, ref)
- project = Project.find_by(id: project_id)
+ project = Project.find_by_id(project_id)
return unless project
- user = User.find_by(id: user_id)
+ user = User.find_by_id(user_id)
return unless user
MergeRequests::RefreshService.new(project: project, current_user: user).execute(oldrev, newrev, ref)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/update_project_statistics_worker.rb b/app/workers/update_project_statistics_worker.rb
index 4a32753ac70..45a6cc8f476 100644
--- a/app/workers/update_project_statistics_worker.rb
+++ b/app/workers/update_project_statistics_worker.rb
@@ -13,11 +13,9 @@ class UpdateProjectStatisticsWorker # rubocop:disable Scalability/IdempotentWork
# project_id - The ID of the project for which to flush the cache.
# statistics - An Array containing columns from ProjectStatistics to
# refresh, if empty all columns will be refreshed
- # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, statistics = [])
- project = Project.find_by(id: project_id)
+ project = Project.find_by_id(project_id)
Projects::UpdateStatisticsService.new(project, nil, statistics: statistics).execute
end
- # rubocop: enable CodeReuse/ActiveRecord
end