diff options
54 files changed, 882 insertions, 233 deletions
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index a2fa2e7769b..c0224d3bfa9 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -1,6 +1,29 @@ +.blank-state-parent-container { + display: flex; + + .section-container { + display: flex; + flex: 1; + padding: 10px; + } + + .section-body { + width: 100%; + height: 100%; + padding-bottom: 25px; + border: 1px solid $border-color; + border-radius: $border-radius-default; + + &.section-ee-trial { + display: flex; + align-items: center; + justify-content: center; + } + } +} + .blank-state-welcome { text-align: center; - border-bottom: 1px solid $border-color; .blank-state-text { margin-bottom: 0; @@ -10,6 +33,10 @@ .blank-state { padding-top: 20px; padding-bottom: 20px; +} + +.blank-state.ee-trial { + padding: 20px; text-align: center; } @@ -20,20 +47,24 @@ .blank-state-icon { padding-bottom: 20px; - color: $gray-darkest; font-size: 56px; - path, - polygon { - fill: currentColor; + svg { + display: block; + margin: auto; + } +} + +@media (min-width: $screen-sm-max) { + .section-welcome .blank-state-icon svg { + width: 130%; } } .blank-state-title { margin-top: 0; - margin-bottom: 5px; + margin-bottom: 10px; font-size: 18px; - font-weight: normal; } .blank-state-text { @@ -49,3 +80,24 @@ .blank-state-welcome-title { font-size: 24px; } + +@media (max-width: $screen-md-min) { + .blank-state-parent-container { + &, + .section-container { + display: block; + } + } + + .blank-state { + text-align: center; + } + + .blank-state-icon { + padding-bottom: 0; + } + + .blank-state-body { + margin-top: 15px; + } +} diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 38727e15c6f..e59cd0eea82 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -343,6 +343,12 @@ ul.indent-list { .group-row { padding: 0; border: none; + + &:last-of-type { + .group-row-contents:not(:hover) { + border-bottom: 1px solid transparent; + } + } } .group-row-contents { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 23555e8e3e6..3f032776d82 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -74,11 +74,17 @@ $red-700: #a62d19; $red-800: #8b2615; $red-900: #711e11; -$purple-600: #6e49cb; -$purple-650: #5c35ae; -$purple-700: #4a2192; -$purple-800: #2c0a5c; -$purple-900: #380d75; +$indigo-50: #f7f7ff; +$indigo-100: #ebebfa; +$indigo-200: #d1d1f0; +$indigo-300: #a6a6de; +$indigo-400: #7c7ccc; +$indigo-500: #6666c4; +$indigo-600: #5b5bbd; +$indigo-700: #4b4ba3; +$indigo-800: #393982; +$indigo-900: #292961; +$indigo-950: #1a1a40; $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index bfb7a0c7e25..73cb3a7cf4c 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -4,7 +4,7 @@ header.navbar-gitlab-new { color: $white-light; - background-color: $purple-900; + background: linear-gradient(to right, $indigo-900, $indigo-800); border-bottom: 0; .header-content { @@ -24,11 +24,9 @@ header.navbar-gitlab-new { > a { display: flex; align-items: center; - padding-top: 3px; padding-right: $gl-padding; padding-left: $gl-padding; margin-left: -$gl-padding; - border-bottom: 3px solid transparent; @media (min-width: $screen-sm-min) { padding-right: $gl-padding; @@ -45,9 +43,8 @@ header.navbar-gitlab-new { &:hover, &:focus { - color: currentColor; + color: $tanuki-yellow; text-decoration: none; - border-bottom-color: $white-light; } } } @@ -71,7 +68,7 @@ header.navbar-gitlab-new { .navbar-collapse { padding-left: 0; - color: $white-light; + color: $indigo-200; box-shadow: 0; @media (max-width: $screen-xs-max) { @@ -101,7 +98,7 @@ header.navbar-gitlab-new { font-size: 14px; text-align: center; color: currentColor; - border-left: 1px solid lighten($purple-700, 10%); + border-left: 1px solid lighten($indigo-700, 10%); &:hover, &:focus, @@ -120,6 +117,7 @@ header.navbar-gitlab-new { li { .badge { box-shadow: none; + font-weight: 600; } } } @@ -133,12 +131,11 @@ header.navbar-gitlab-new { > a { background: none; - opacity: .9; - will-change: opacity; + will-change: color; &.header-user-dropdown-toggle { .header-user-avatar { - border-color: $white-light; + border-color: $indigo-200; } } @@ -165,29 +162,34 @@ header.navbar-gitlab-new { .navbar-sub-nav { display: flex; margin-bottom: 0; - color: $white-light; + color: $indigo-200; > li { - &.active > a, - a:hover, - a:focus { - border-bottom-color: $white-light; + > a:hover, + > a:focus { + box-shadow: inset 0 -3px 0 rgba($indigo-200, .4); text-decoration: none; outline: 0; - opacity: 1; + color: $white-light; + } + + &.active > a { + box-shadow: inset 0 -3px 0 $indigo-500; + color: $white-light; + font-weight: 700; } > a { display: block; - padding: 16px 10px 13px; + padding: 16px 10px; font-size: 13px; color: currentColor; - border-bottom: 3px solid transparent; - opacity: .9; - will-change: opacity; + box-shadow: inset 0 0 0 transparent; + will-change: box-shadow; + transition: box-shadow 0.15s; @media (min-width: $screen-sm-min) { - padding: 15px $gl-padding 12px; + padding: 15px $gl-padding; font-size: 14px; } } @@ -207,55 +209,60 @@ header.navbar-gitlab-new { .search { form { - border-color: $purple-800; + border: 0; + background-color: rgba($indigo-200, .2); + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, background-color ease-in-out 0.15s; &:hover { - border-color: rgba($white-light, .6); + background-color: rgba($indigo-200, .3); box-shadow: none; } } &.search-active form { - border-color: $white-light; - } - - form, - .search-input { - background-color: $purple-700; + background-color: rgba($indigo-200, .3); + box-shadow: none; } .search-input { color: $white-light; + background: none; } .search-input::placeholder { - color: rgba($white-light, .6); + color: rgba($indigo-200, .8); } .location-badge { font-size: 12px; - color: rgba($white-light, .6); - background-color: $purple-800; + color: $indigo-100; + background-color: rgba($indigo-200, .1); transition: color 0.15s; will-change: color; + margin: -4px 4px -4px -4px; + line-height: 25px; + padding: 4px 8px; + border-radius: 2px 0 0 2px; + border-right: 1px solid $indigo-800; + height: 34px; } .search-input-wrap { .search-icon, .clear-icon { - color: rgba($white-light, .6); + color: rgba($indigo-200, .8); } } &.search-active { .location-badge { color: $white-light; - background-color: $purple-800; + background-color: rgba($indigo-200, .2); } .search-input-wrap { .search-icon { - color: rgba($white-light, .6); + color: rgba($indigo-200, .8); } .clear-icon { diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index f9ceaec9418..96459fe31cc 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -2,6 +2,15 @@ @import 'framework/tw_bootstrap_variables'; @import "bootstrap/variables"; +$active-background: rgba(0,0,0,.04); +$active-border: $indigo-500; +$active-color: $indigo-700; +$active-hover-background: $active-background; +$active-hover-color: $gl-text-color; +$inactive-badge-background: rgba(0,0,0,.08); +$hover-background: $indigo-700; +$hover-color: $white-light; +$inactive-color: $gl-text-color-secondary; $new-sidebar-width: 220px; .page-with-new-sidebar { @@ -17,19 +26,33 @@ $new-sidebar-width: 220px; } .context-header { - background-color: $gray-normal; border-bottom: 1px solid $border-color; font-weight: 600; display: flex; align-items: center; - padding: 10px 14px; + padding: 10px 16px 10px 10px; + color: $gl-text-color; .avatar-container { flex: 0 0 40px; } &:hover { - background-color: $border-color; + background-color: $hover-background; + color: $hover-color; + border-color: $hover-background; + + .avatar-container { + border-color: transparent; + } + + .settings-avatar { + background-color: $indigo-500; + + i { + color: $hover-color; + } + } } .project-title, @@ -41,6 +64,7 @@ $new-sidebar-width: 220px; .settings-avatar { background-color: $white-light; + transition: background-color 100ms linear; i { font-size: 20px; @@ -48,6 +72,7 @@ $new-sidebar-width: 220px; color: $gl-text-color-secondary; text-align: center; align-self: center; + transition: color 100ms linear; } } @@ -60,11 +85,15 @@ $new-sidebar-width: 220px; bottom: 0; left: 0; overflow: auto; - background-color: $gray-light; - border-right: 1px solid $border-color; + background-color: $gray-normal; + box-shadow: inset -2px 0 0 $border-color; + + a { + text-decoration: none; + } ul { - padding: 0; + padding-left: 0; list-style: none; } @@ -73,13 +102,18 @@ $new-sidebar-width: 220px; a { display: block; - padding: 12px 14px; + padding: 12px 16px; + color: $inactive-color; } } - a { - color: $gl-text-color; - text-decoration: none; + li.active { + box-shadow: inset 4px 0 0 $active-border; + + > a { + color: $active-color; + font-weight: 700; + } } @media (max-width: $screen-xs-max) { @@ -89,22 +123,28 @@ $new-sidebar-width: 220px; .sidebar-sub-level-items { display: none; + padding-bottom: 8px; > li { a { - padding: 12px 24px; - color: $gl-text-color-light; + font-size: 12px; + padding: 8px 16px 8px 24px; - &:hover { - color: $gl-text-color; - background-color: $border-color; + &:hover, + &:focus { + background: $active-hover-background; + color: $active-hover-color; } } &.active { - > a { - color: $purple-650; - font-weight: 600; + a { + &, + &:hover, + &:focus { + background: $active-background; + color: $active-color; + } } } } @@ -114,35 +154,31 @@ $new-sidebar-width: 220px; > li { .badge { float: right; - background-color: $border-color; - color: $gl-text-color; + background-color: $inactive-badge-background; + color: $inactive-color; } &.active { - > a { - background-color: $purple-600; - color: $white-light; - font-weight: 600; - } + background: $active-background; .badge { - background-color: $purple-700; - color: $white-light; + color: $active-color; + font-weight: 600; } .sidebar-sub-level-items { - background-color: $gray-normal; - border-left: 6px solid $purple-600; display: block; } } - &:not(.active) > a:hover { - background-color: $border-color; + > a:hover { + background-color: $hover-background; + color: $hover-color; .badge { - transition: background-color 100ms linear; - background-color: $gray-normal; + transition: background-color 100ms linear, color 100ms linear; + background-color: $indigo-500; + color: $hover-color; } } } @@ -161,3 +197,13 @@ $new-sidebar-width: 220px; // scss-lint:enable DuplicateProperty } } + + +// Change color of all horizontal tabs to match the new indigo color +.nav-links li.active a { + border-bottom-color: $active-border; + + .badge { + font-weight: 600; + } +} diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index f21005895e4..e7c07ef67f0 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -54,8 +54,6 @@ @media (min-width: $screen-sm-min) { display: -webkit-flex; display: flex; - width: 400px; - max-width: 50%; } } @@ -65,7 +63,6 @@ @media (min-width: $screen-sm-min) { display: -webkit-flex; display: flex; - width: 100%; margin-top: 3px; } } @@ -81,18 +78,10 @@ .member-form-control { @media (max-width: $screen-xs-max) { - padding: 5px 0; + padding-bottom: 5px; margin-left: 0; margin-right: 0; } - - @media (min-width: $screen-sm-min) { - width: 50%; - } - - .dropdown-menu-toggle { - width: 100%; - } } .member-access-text { @@ -216,3 +205,102 @@ } } } + +.content-list.members-list li { + display: flex; + justify-content: space-between; + + .list-item-name { + float: none; + display: flex; + flex: 1; + } + + .user-info { + padding-right: 10px; + } + + .member { + font-weight: bold; + overflow-wrap: break-word; + word-break: break-all; + } + + .member-group-link { + display: inline-block; + } + + .form-control { + width: inherit; + } + + .btn { + align-self: flex-start; + } + + .form-horizontal ~ .btn { + margin-right: 0; + } + + @media (max-width: $screen-xs-max) { + display: block; + + .controls > .btn { + margin-left: 0; + margin-right: 0; + display: block; + } + + .form-control { + width: 100%; + } + + .member-access-text { + line-height: 0; + margin-left: 50px; + } + + .member-controls { + margin-top: 5px; + } + + .form-horizontal { + margin-top: 10px; + } + } +} + +.panel-mobile { + .content-list.members-list li { + display: block; + + .member-controls { + float: none; + display: block; + } + + .dropdown-menu-toggle, + .dropdown-menu, + .form-control, + .list-item-name { + width: 100%; + } + + .dropdown-menu { + margin-top: 0; + } + + .form-horizontal { + display: block; + } + + .member-form-control { + margin: 5px 0; + } + + .btn { + width: 100%; + margin-left: 0; + } + } +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index c207159f606..235c475ff26 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -286,8 +286,7 @@ table.u2f-registrations { } .user-callout { - margin: 0 auto; - max-width: $screen-lg-min; + margin: 20px -5px 0; .bordered-box { border: 1px solid $blue-300; diff --git a/app/finders/concerns/created_at_filter.rb b/app/finders/concerns/created_at_filter.rb new file mode 100644 index 00000000000..ac9ac77732c --- /dev/null +++ b/app/finders/concerns/created_at_filter.rb @@ -0,0 +1,8 @@ +module CreatedAtFilter + def by_created_at(items) + items = items.created_before(params[:created_before]) if params[:created_before].present? + items = items.created_after(params[:created_after]) if params[:created_after].present? + + items + end +end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index d81e9ed17d4..2e5a6493134 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -19,6 +19,8 @@ # iids: integer[] # class IssuableFinder + include CreatedAtFilter + NONE = '0'.freeze IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze @@ -32,6 +34,7 @@ class IssuableFinder def execute items = init_collection items = by_scope(items) + items = by_created_at(items) items = by_state(items) items = by_group(items) items = by_search(items) @@ -42,7 +45,6 @@ class IssuableFinder items = by_iids(items) items = by_milestone(items) items = by_label(items) - items = by_created_at(items) # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far items = by_project(items) @@ -411,18 +413,6 @@ class IssuableFinder params[:non_archived].present? ? items.non_archived : items end - def by_created_at(items) - if params[:created_after].present? - items = items.where(items.klass.arel_table[:created_at].gteq(params[:created_after])) - end - - if params[:created_before].present? - items = items.where(items.klass.arel_table[:created_at].lteq(params[:created_before])) - end - - items - end - def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index 23c42a5f662..0a5a0ea2f35 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -49,7 +49,8 @@ class MilestonesFinder if params.has_key?(:order) items.reorder(params[:order]) else - items.reorder('due_date ASC') + order_statement = Gitlab::Database.nulls_last_order('due_date', 'ASC') + items.reorder(order_statement) end end end diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index 07deceb827b..33f7ae90598 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -14,6 +14,8 @@ # external: boolean # class UsersFinder + include CreatedAtFilter + attr_accessor :current_user, :params def initialize(current_user, params = {}) @@ -29,6 +31,7 @@ class UsersFinder users = by_active(users) users = by_external_identity(users) users = by_external(users) + users = by_created_at(users) users end diff --git a/app/models/concerns/created_at_filterable.rb b/app/models/concerns/created_at_filterable.rb new file mode 100644 index 00000000000..e8a3e41203d --- /dev/null +++ b/app/models/concerns/created_at_filterable.rb @@ -0,0 +1,12 @@ +module CreatedAtFilterable + extend ActiveSupport::Concern + + included do + scope :created_before, ->(date) { where(scoped_table[:created_at].lteq(date)) } + scope :created_after, ->(date) { where(scoped_table[:created_at].gteq(date)) } + + def self.scoped_table + arel_table.alias(table_name) + end + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 01f985823e1..400bb55d2f0 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -10,6 +10,7 @@ class Issue < ActiveRecord::Base include FasterCacheKeys include RelativePositioning include IgnorableColumn + include CreatedAtFilterable ignore_column :position @@ -50,8 +51,6 @@ class Issue < ActiveRecord::Base scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } - scope :created_after, -> (datetime) { where("created_at >= ?", datetime) } - scope :preload_associations, -> { preload(:labels, project: :namespace) } after_save :expire_etag_cache diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6ea774470af..815c5b43406 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -5,6 +5,7 @@ class MergeRequest < ActiveRecord::Base include Referable include Sortable include IgnorableColumn + include CreatedAtFilterable ignore_column :position @@ -14,7 +15,7 @@ class MergeRequest < ActiveRecord::Base has_many :merge_request_diffs has_one :merge_request_diff, - -> { order('merge_request_diffs.id DESC') } + -> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline" diff --git a/app/models/project.rb b/app/models/project.rb index 74c15d2508b..d0e02e4e585 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -536,6 +536,11 @@ class Project < ActiveRecord::Base ProjectCacheWorker.perform_async(self.id) end + remove_import_data + end + + # This method is overriden in EE::Project model + def remove_import_data import_data&.destroy end diff --git a/app/models/user.rb b/app/models/user.rb index 4411a06d429..4b01c2f19f0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -12,6 +12,7 @@ class User < ActiveRecord::Base include TokenAuthenticatable include IgnorableColumn include FeatureGate + include CreatedAtFilterable DEFAULT_NOTIFICATION_LEVEL = :participating diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 9149b8e7fb9..843c71af466 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -107,8 +107,7 @@ = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" %hr = button_tag 'Add users to group', class: "btn btn-create" - - = render 'shared/members/requests', membership_source: @group, requesters: @requesters + = render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true .panel.panel-default .panel-heading @@ -117,7 +116,7 @@ %span.badge= @group.members.size .pull-right = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-xs" - %ul.well-list.group-users-list.content-list + %ul.well-list.group-users-list.content-list.members-list = render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false } .panel-footer = paginate @members, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index fb9057b2db5..7b1b15cfeb8 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -160,12 +160,12 @@ .pull-right = link_to admin_group_path(@group), class: 'btn btn-xs' do = icon('pencil-square-o', text: 'Manage access') - %ul.well-list.content-list + %ul.well-list.content-list.members-list = render partial: 'shared/members/member', collection: @group_members, as: :member, locals: { show_controls: false } .panel-footer = paginate @group_members, param_name: 'group_members_page', theme: 'gitlab' - = render 'shared/members/requests', membership_source: @project, requesters: @requesters + = render 'shared/members/requests', membership_source: @project, requesters: @requesters, force_mobile_view: true .panel.panel-default .panel-heading @@ -174,7 +174,7 @@ %span.badge= @project.users.size .pull-right = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-xs" - %ul.well-list.project_members.content-list + %ul.well-list.project_members.content-list.members-list = render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false } .panel-footer = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml new file mode 100644 index 00000000000..0319838bdb4 --- /dev/null +++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml @@ -0,0 +1,33 @@ +.row.blank-state.clearfix + .col-md-1.col-md-offset-3.blank-state-icon + = custom_icon("add_new_user", size: 50) + .col-md-5.blank-state-body + %h3.blank-state-title + Add user + %p.blank-state-text + Add your team members and others to GitLab. + = link_to new_admin_user_path, class: "btn btn-new" do + New user + +.row.blank-state.clearfix + .col-md-1.col-md-offset-3.blank-state-icon + = custom_icon("configure_server", size: 50) + .col-md-5.blank-state-body + %h3.blank-state-title + Configure GitLab + %p.blank-state-text + Make adjustments to how your GitLab instance is set up. + = link_to admin_root_path, class: "btn btn-new" do + Configure + +- if current_user.can_create_group? + .row.blank-state.clearfix + .col-md-1.col-md-offset-3.blank-state-icon + = custom_icon("add_new_group", size: 50) + .col-md-5.blank-state-body + %h3.blank-state-title + Create a group + %p.blank-state-text + Groups are a great way to organise projects and people. + = link_to new_group_path, class: "btn btn-new" do + New group diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml new file mode 100644 index 00000000000..a079f0ac1a4 --- /dev/null +++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml @@ -0,0 +1,48 @@ +- public_project_count = ProjectsFinder.new(current_user: current_user).execute.count + +- if current_user.can_create_group? + .row.blank-state.clearfix + .col-md-1.col-md-offset-3.blank-state-icon + = custom_icon("add_new_group", size: 50) + .col-md-5.blank-state-body + %h3.blank-state-title + Create a group for several dependent projects. + %p.blank-state-text + Groups are the best way to manage projects and members. + = link_to new_group_path, class: "btn btn-new" do + New group + +.row.blank-state.clearfix + .col-md-1.col-md-offset-3.blank-state-icon + = custom_icon("add_new_project", size: 50) + .col-md-5.blank-state-body + %h3.blank-state-title + Create a project + %p.blank-state-text + - if current_user.can_create_project? + You don't have access to any projects right now. + You can create up to + %strong= number_with_delimiter(current_user.projects_limit) + = succeed "." do + = "project".pluralize(current_user.projects_limit) + - else + If you are added to a project, it will be displayed here. + - if current_user.can_create_project? + = link_to new_project_path, class: "btn btn-new" do + New project + +- if public_project_count > 0 + .row.blank-state.clearfix + .col-md-1.col-md-offset-3.blank-state-icon + = custom_icon("globe", size: 50) + .col-md-5.blank-state-body + %h3.blank-state-title + Explore public projects + %p.blank-state-text + There are + = number_with_delimiter(public_project_count) + public projects on this server. + Public projects are an easy way to allow + everyone to have read-only access. + = link_to trending_explore_projects_path, class: "btn btn-new" do + Browse projects diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index 8843d4e7c84..94af033c1e3 100644 --- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml @@ -1,47 +1,12 @@ -- publicish_project_count = ProjectsFinder.new(current_user: current_user).execute.count -.blank-state.blank-state-welcome - %h2.blank-state-welcome-title - Welcome to GitLab - %p.blank-state-text - Code, test, and deploy together - -- if current_user.can_create_group? - .blank-state - .blank-state-icon - = custom_icon("group", size: 50) - %h3.blank-state-title - You can create a group for several dependent projects. - %p.blank-state-text - Groups are the best way to manage projects and members. - = link_to new_group_path, class: "btn btn-new" do - New group - -.blank-state - .blank-state-icon - = custom_icon("project", size: 50) - %h3.blank-state-title - You don't have access to any projects right now - %p.blank-state-text - - if current_user.can_create_project? - You can create up to - %strong= number_with_delimiter(current_user.projects_limit) - = succeed "." do - = "project".pluralize(current_user.projects_limit) - - else - If you are added to a project, it will be displayed here. - - if current_user.can_create_project? - = link_to new_project_path, class: "btn btn-new" do - New project - -- if publicish_project_count > 0 - .blank-state - .blank-state-icon - = icon("globe") - %h3.blank-state-title - There are - = number_with_delimiter(publicish_project_count) - public projects on this server. - %p.blank-state-text - Public projects are an easy way to allow everyone to have read-only access. - = link_to trending_explore_projects_path, class: "btn btn-new" do - Browse projects +.row.blank-state-parent-container + .section-container + .container.section-body.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" } + .blank-state.blank-state-welcome + %h2.blank-state-welcome-title + Welcome to GitLab + %p.blank-state-text + Code, test, and deploy together + - if current_user.admin? + = render "blank_state_admin_welcome" + - else + = render "blank_state_welcome" diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 2e4e4511bb6..ad9d5562ded 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -27,6 +27,6 @@ Members with access to %strong= @group.name %span.badge= @members.total_count - %ul.content-list + %ul.content-list.members-list = render partial: 'shared/members/member', collection: @members, as: :member = paginate @members, theme: 'gitlab' diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 1f18490594b..e71d58ec26d 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -11,5 +11,5 @@ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } = icon("search") = render 'shared/members/sort_dropdown' - %ul.content-list + %ul.content-list.members-list = render partial: 'shared/members/member', collection: members, as: :member diff --git a/app/views/shared/icons/_add_new_group.svg b/app/views/shared/icons/_add_new_group.svg new file mode 100644 index 00000000000..ecd52c5e99f --- /dev/null +++ b/app/views/shared/icons/_add_new_group.svg @@ -0,0 +1,8 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"> + <g fill="none" fill-rule="evenodd"> + <path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/> + <path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/> + <path fill="#E1DBF2" fill-rule="nonzero" d="M59.65 32.65H60l-2-2.42-2 2.4-2-2.4-2 2.4-2-2.4-2 2.4-2-2.4-2 2.42h.77C45.57 34.6 46 36.75 46 39c0 2.84-.7 5.5-1.92 7.86 1.97 2.28 4.83 3.64 7.92 3.64 5.8 0 10.5-4.74 10.5-10.6 0-2.8-1.08-5.36-2.85-7.25zM43.18 29.6c2.4-2.1 5.52-3.3 8.82-3.3 7.46 0 13.5 6.1 13.5 13.6S59.46 53.5 52 53.5c-3.68 0-7.1-1.5-9.6-4.04C39.3 53.44 34.44 56 29 56c-9.4 0-17-7.6-17-17s7.6-17 17-17c3.22 0 6.23.9 8.8 2.45 2.13 1.3 3.97 3.05 5.38 5.16zM17 34c-.65 1.54-1 3.23-1 5 0 7.18 5.82 13 13 13s13-5.82 13-13c0-1.77-.35-3.46-1-5h-9c-.53 0-1.04-.2-1.4-.6L29 31.84l-1.6 1.58c-.36.4-.87.6-1.4.6h-9zm21.38-4c-2.4-2.5-5.76-4-9.38-4-3.62 0-6.98 1.5-9.38 4h5.55l2.42-2.4c.74-.8 2-.8 2.8 0l2.4 2.4h5.54z"/> + <path fill="#6B4FBB" d="M47.6 42.32c-.66 0-1.2-.54-1.2-1.2 0-.68.54-1.22 1.2-1.22.66 0 1.2.54 1.2 1.2 0 .68-.54 1.22-1.2 1.22zm8.8 0c-.66 0-1.2-.54-1.2-1.2 0-.68.54-1.22 1.2-1.22.66 0 1.2.54 1.2 1.2 0 .68-.54 1.22-1.2 1.22zM25 44h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-1c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/> + </g> +</svg> diff --git a/app/views/shared/icons/_add_new_project.svg b/app/views/shared/icons/_add_new_project.svg new file mode 100644 index 00000000000..3c1e15453df --- /dev/null +++ b/app/views/shared/icons/_add_new_project.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M30 24a4 4 0 0 0-4 4v22a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V28a4 4 0 0 0-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#FC6D26" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_add_new_user.svg b/app/views/shared/icons/_add_new_user.svg new file mode 100644 index 00000000000..0ad40498d7b --- /dev/null +++ b/app/views/shared/icons/_add_new_user.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"> + <g fill="none" fill-rule="evenodd"> + <path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/> + <path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/> + <path fill="#E1DBF2" d="M44 31l-2.5-3-2.5 3-2.5-3-2.5 3-2.5-3-2.5 3h-2.72c2.65-4.2 7.36-7 12.72-7s10.07 2.8 12.72 7H49l-2.5-3-2.5 3z"/> + <path fill="#E1DBF2" fill-rule="nonzero" d="M39 57c-9.4 0-17-7.6-17-17s7.6-17 17-17 17 7.6 17 17-7.6 17-17 17zm0-4c7.18 0 13-5.82 13-13s-5.82-13-13-13-13 5.82-13 13 5.82 13 13 13z"/> + <path fill="#6B4FBB" d="M35 45h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/> + </g> +</svg> diff --git a/app/views/shared/icons/_configure_server.svg b/app/views/shared/icons/_configure_server.svg new file mode 100644 index 00000000000..b1137b7ec94 --- /dev/null +++ b/app/views/shared/icons/_configure_server.svg @@ -0,0 +1,8 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"> + <g fill="none" fill-rule="evenodd"> + <path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/> + <path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/> + <path fill="#FEE1D3" fill-rule="nonzero" d="M24.92 35.15c-1.72-1.4-1.98-3.9-.6-5.63l1.26-1.55c1.4-1.72 3.9-2 5.63-.6l.7.56c.7-.4 1.4-.73 2.1-1V26c0-2.2 1.8-4 4-4h2c2.2 0 4 1.8 4 4v.92c.8.28 1.5.62 2.1 1l.7-.55c1.7-1.4 4.3-1.12 5.7.6l1.3 1.55c1.4 1.72 1.2 4.23-.6 5.63l-.7.6c.3.74.4 1.5.5 2.3l.9.2c2.2.5 3.5 2.64 3 4.8L56.4 45c-.5 2.15-2.64 3.5-4.8 3l-.88-.2c-.44.63-.92 1.24-1.46 1.8l.4.82c.9 1.98.1 4.38-1.9 5.35l-1.8.87c-2 .97-4.37.15-5.34-1.84l-.46-.85c-.34.03-.74.05-1.13.05-.4 0-.8-.02-1.2-.05l-.4.85c-.95 2-3.34 2.8-5.33 1.84l-1.8-.87c-1.97-.97-2.8-3.37-1.83-5.35l.4-.8c-.54-.58-1.02-1.2-1.46-1.83l-.8.2c-2.2.5-4.3-.9-4.8-3l-.4-2c-.5-2.2.85-4.3 3-4.8l.9-.2c.1-.8.3-1.6.5-2.3l-.7-.6zm4.95.77c-.53 1.2-.83 2.47-.87 3.8-.02.9-.66 1.68-1.55 1.9l-2.32.53.45 1.94 2.3-.6c.9-.2 1.8.2 2.23 1 .7 1.1 1.5 2.2 2.5 3 .7.6.9 1.6.5 2.4l-1 2.1 1.8.9 1.1-2.1c.4-.8 1.3-1.3 2.2-1.1.7.1 1.3.2 2 .2s1.3-.1 2-.2c.9-.2 1.8.3 2.2 1.1l1 2.1 1.8-.9-1.2-2c-.4-.8-.2-1.8.5-2.4 1-.85 1.84-1.88 2.45-3.05.4-.82 1.33-1.24 2.2-1.04l2.33.54.45-1.95-2.32-.54c-.9-.2-1.52-.97-1.54-1.88-.03-1.4-.33-2.6-.86-3.8-.4-.9-.2-1.8.5-2.4l1.9-1.5-1.3-1.6-1.8 1.5c-.8.5-1.8.6-2.5 0-1.1-.8-2.3-1.4-3.5-1.7-.9-.2-1.5-1-1.5-1.9V26h-2v2.38c0 .9-.6 1.7-1.5 1.93-1.3.4-2.5 1-3.5 1.7-.8.6-1.8.6-2.5 0l-1.9-1.5-1.26 1.6 1.8 1.5c.7.6.94 1.6.6 2.4z"/> + <path fill="#FC6D26" fill-rule="nonzero" d="M39 46c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/> + </g> +</svg> diff --git a/app/views/shared/icons/_globe.svg b/app/views/shared/icons/_globe.svg new file mode 100644 index 00000000000..c2daae5f317 --- /dev/null +++ b/app/views/shared/icons/_globe.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" d="M30.24 27.823A14.98 14.98 0 0 0 24 40c0 2.549.636 4.949 1.757 7.051-.297-2.684.644-4.026 2.823-4.026 3.707 0 2.462 5.365 4.473 5.761 2.01.396 4.175.396 4.267 3.29.04 1.257-.265 2.157-.917 2.7a15.095 15.095 0 0 0 8.555-1.006c.035-1.91.303-4.941 2.21-5.61 2.373-.833-.55-1.431.734-3.368 1.17-1.762-3.297-5.2 0-4.832 3.477.388 5.044-.816 6.024-1.456a14.903 14.903 0 0 0-1.373-4.94c-.873.4-2.19.465-3.702-.538-.757-.502-1.084-3.944-2.107-3.944-3.823 0-4.065 3.17-5.994 3.944-1.076.431-4.193 3.773-5.614 3.596-1.126-.14-1.071-4.417-2.45-5.166-1.359-.738-2.174-1.948-2.447-3.633zM39 59c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19z"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index a5aa768b1b2..951b4dd7b36 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -1,5 +1,6 @@ - show_roles = local_assigns.fetch(:show_roles, true) - show_controls = local_assigns.fetch(:show_controls, true) +- force_mobile_view = local_assigns.fetch(:force_mobile_view, false) - user = local_assigns.fetch(:user, member.user) - source = member.source - can_admin_member = can?(current_user, action_member_permission(:update, member), member) @@ -8,45 +9,53 @@ %span.list-item-name - if user = image_tag avatar_icon(user, 40), class: "avatar s40", alt: '' - %strong - = link_to user.name, user_path(user) - %span.cgray= user.to_reference + .user-info + = link_to user.name, user_path(user), class: 'member' + %span.cgray= user.to_reference - - if user == current_user - %span.label.label-success.prepend-left-5 It's you + - if user == current_user + %span.label.label-success.prepend-left-5 It's you - - if user.blocked? - %label.label.label-danger - %strong Blocked + - if user.blocked? + %label.label.label-danger + %strong Blocked - - if source.instance_of?(Group) && source != @group - · - = link_to source.full_name, source, class: "member-group-link" + - if source.instance_of?(Group) && source != @group + · + = link_to source.full_name, source, class: "member-group-link" - .hidden-xs.cgray - - if member.request? - Requested - = time_ago_with_tooltip(member.requested_at) - - else - Joined #{time_ago_with_tooltip(member.created_at)} - - if member.expires? - · - %span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } - Expires in #{distance_of_time_in_words_to_now(member.expires_at)} + .cgray + - if member.request? + Requested + = time_ago_with_tooltip(member.requested_at) + - else + Joined #{time_ago_with_tooltip(member.created_at)} + - if member.expires? + · + %span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } + Expires in #{distance_of_time_in_words_to_now(member.expires_at)} - else = image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: '' - %strong= member.invite_email - .cgray - Invited - - if member.created_by - by - = link_to member.created_by.name, user_path(member.created_by) - = time_ago_with_tooltip(member.created_at) + .user-info + .member= member.invite_email + .cgray + Invited + - if member.created_by + by + = link_to member.created_by.name, user_path(member.created_by) + = time_ago_with_tooltip(member.created_at) - if show_roles - current_resource = @project || @group .controls.member-controls - if show_controls && member.source == current_resource + + - if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source) + = link_to icon('paper-plane'), polymorphic_path([:resend_invite, member]), + method: :post, + class: 'btn btn-default prepend-left-10 hidden-xs', + title: 'Resend invite' + - if user != current_user && can_admin_member = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.hidden_field :access_level @@ -75,13 +84,17 @@ - if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source) = link_to 'Resend invite', polymorphic_path([:resend_invite, member]), method: :post, - class: 'btn btn-default prepend-left-10' + class: 'btn btn-default prepend-left-10 visible-xs-block' - elsif member.request? && can_admin_member - = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]), + = link_to polymorphic_path([:approve_access_request, member]), method: :post, class: 'btn btn-success prepend-left-10', - title: 'Grant access' + title: 'Grant access' do + %span{ class: ('visible-xs-block' unless force_mobile_view) } + Grant access + - unless force_mobile_view + = icon('check inverse', class: 'hidden-xs') - if can?(current_user, action_member_permission(:destroy, member), member) - if current_user == user @@ -96,8 +109,9 @@ data: { confirm: remove_member_message(member) }, class: 'btn btn-remove prepend-left-10', title: remove_member_title(member) do - %span.visible-xs-block + %span{ class: ('visible-xs-block' unless force_mobile_view) } Delete - = icon('trash', class: 'hidden-xs') + - unless force_mobile_view + = icon('trash', class: 'hidden-xs') - else %span.member-access-text= member.human_access diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml index 92f6e7428ae..09b9944082f 100644 --- a/app/views/shared/members/_requests.html.haml +++ b/app/views/shared/members/_requests.html.haml @@ -1,8 +1,10 @@ +- force_mobile_view = local_assigns.fetch(:force_mobile_view, false) + - if requesters.any? - .panel.panel-default.prepend-top-default + .panel.panel-default.prepend-top-default{ class: ('panel-mobile' if force_mobile_view ) } .panel-heading Users requesting access to %strong= membership_source.name %span.badge= requesters.size - %ul.content-list - = render partial: 'shared/members/member', collection: requesters, as: :member + %ul.content-list.members-list + = render partial: 'shared/members/member', collection: requesters, as: :member, locals: { force_mobile_view: force_mobile_view } diff --git a/app/workers/background_migration_worker.rb b/app/workers/background_migration_worker.rb index e85e221d353..45ce49bb5c0 100644 --- a/app/workers/background_migration_worker.rb +++ b/app/workers/background_migration_worker.rb @@ -2,18 +2,34 @@ class BackgroundMigrationWorker include Sidekiq::Worker include DedicatedSidekiqQueue - # Schedules a number of jobs in bulk + # Enqueues a number of jobs in bulk. # # The `jobs` argument should be an Array of Arrays, each sub-array must be in # the form: # # [migration-class, [arg1, arg2, ...]] - def self.perform_bulk(*jobs) + def self.perform_bulk(jobs) Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => jobs) end + # Schedules multiple jobs in bulk, with a delay. + # + def self.perform_bulk_in(delay, jobs) + now = Time.now.to_i + schedule = now + delay.to_i + + if schedule <= now + raise ArgumentError, 'The schedule time must be in the future!' + end + + Sidekiq::Client.push_bulk('class' => self, + 'queue' => sidekiq_options['queue'], + 'args' => jobs, + 'at' => schedule) + end + # Performs the background migration. # # See Gitlab::BackgroundMigration.perform for more information. diff --git a/changelogs/unreleased/2501-ce-port-update-welcome-page.yml b/changelogs/unreleased/2501-ce-port-update-welcome-page.yml new file mode 100644 index 00000000000..cac8a522308 --- /dev/null +++ b/changelogs/unreleased/2501-ce-port-update-welcome-page.yml @@ -0,0 +1,4 @@ +--- +title: Update welcome page UX for new users +merge_request: 12662 +author: diff --git a/changelogs/unreleased/25103-mobile-members-page-user-avatar-is-misaligned.yml b/changelogs/unreleased/25103-mobile-members-page-user-avatar-is-misaligned.yml new file mode 100644 index 00000000000..6688e79588f --- /dev/null +++ b/changelogs/unreleased/25103-mobile-members-page-user-avatar-is-misaligned.yml @@ -0,0 +1,4 @@ +--- +title: Improve members view on mobile +merge_request: 12619 +author: diff --git a/changelogs/unreleased/feature-user-datetime-search-api-mysql.yml b/changelogs/unreleased/feature-user-datetime-search-api-mysql.yml new file mode 100644 index 00000000000..27ac50c6cc2 --- /dev/null +++ b/changelogs/unreleased/feature-user-datetime-search-api-mysql.yml @@ -0,0 +1,4 @@ +--- +title: Add creation time filters to user search API for admins +merge_request: 12682 +author: diff --git a/changelogs/unreleased/username-password-stripped-from-import-url-fix.yml b/changelogs/unreleased/username-password-stripped-from-import-url-fix.yml new file mode 100644 index 00000000000..571279d3dc7 --- /dev/null +++ b/changelogs/unreleased/username-password-stripped-from-import-url-fix.yml @@ -0,0 +1,4 @@ +--- +title: Username and password are no longer stripped from import url on mirror update +merge_request: 12725 +author: diff --git a/config/application.rb b/config/application.rb index d88740ef8d7..2f4e2624195 100644 --- a/config/application.rb +++ b/config/application.rb @@ -26,7 +26,8 @@ module Gitlab #{config.root}/app/models/members #{config.root}/app/models/project_services #{config.root}/app/workers/concerns - #{config.root}/app/services/concerns)) + #{config.root}/app/services/concerns + #{config.root}/app/finders/concerns)) config.generators.templates.push("#{config.root}/generator_templates") diff --git a/db/migrate/20170723183807_add_group_id_to_milestones.rb b/db/migrate/20170707183807_add_group_id_to_milestones.rb index e46fc4f80f0..675ffd4a1c9 100644 --- a/db/migrate/20170723183807_add_group_id_to_milestones.rb +++ b/db/migrate/20170707183807_add_group_id_to_milestones.rb @@ -2,6 +2,8 @@ class AddGroupIdToMilestones < ActiveRecord::Migration DOWNTIME = false def up + return if column_exists? :milestones, :group_id + change_column_null :milestones, :project_id, true add_column :milestones, :group_id, :integer diff --git a/db/migrate/20170724184243_add_group_milestone_id_indexes.rb b/db/migrate/20170707184243_add_group_milestone_id_indexes.rb index d48b1884179..aa48fe90cad 100644 --- a/db/migrate/20170724184243_add_group_milestone_id_indexes.rb +++ b/db/migrate/20170707184243_add_group_milestone_id_indexes.rb @@ -6,6 +6,8 @@ class AddGroupMilestoneIdIndexes < ActiveRecord::Migration DOWNTIME = false def up + return if index_exists?(:milestones, :group_id) + add_concurrent_foreign_key :milestones, :namespaces, column: :group_id, on_delete: :cascade add_concurrent_index :milestones, :group_id diff --git a/db/migrate/20170707184244_remove_wrong_versions_from_schema_versions.rb b/db/migrate/20170707184244_remove_wrong_versions_from_schema_versions.rb new file mode 100644 index 00000000000..38536a8b06a --- /dev/null +++ b/db/migrate/20170707184244_remove_wrong_versions_from_schema_versions.rb @@ -0,0 +1,10 @@ +class RemoveWrongVersionsFromSchemaVersions < ActiveRecord::Migration + DOWNTIME = false + + def up + execute("DELETE FROM schema_migrations WHERE version IN ('20170723183807', '20170724184243')") + end + + def down + end +end diff --git a/db/post_migrate/20170628080858_migrate_stage_id_reference_in_background.rb b/db/post_migrate/20170628080858_migrate_stage_id_reference_in_background.rb new file mode 100644 index 00000000000..f31015d77a3 --- /dev/null +++ b/db/post_migrate/20170628080858_migrate_stage_id_reference_in_background.rb @@ -0,0 +1,33 @@ +class MigrateStageIdReferenceInBackground < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 10000 + RANGE_SIZE = 1000 + MIGRATION = 'MigrateBuildStageIdReference'.freeze + + disable_ddl_transaction! + + class Build < ActiveRecord::Base + self.table_name = 'ci_builds' + include ::EachBatch + end + + ## + # It will take around 3 days to process 20M ci_builds. + # + def up + Build.where(stage_id: nil).each_batch(of: BATCH_SIZE) do |relation, index| + relation.each_batch(of: RANGE_SIZE) do |relation| + range = relation.pluck('MIN(id)', 'MAX(id)').first + + BackgroundMigrationWorker + .perform_in(index * 2.minutes, MIGRATION, range) + end + end + end + + def down + # noop + end +end diff --git a/db/schema.rb b/db/schema.rb index 023783c2b3b..3dbe52c9c80 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170724184243) do +ActiveRecord::Schema.define(version: 20170707184244) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/doc/api/users.md b/doc/api/users.md index cf09b8f44aa..91170e79645 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -146,6 +146,12 @@ GET /users?extern_uid=1234567&provider=github You can search for users who are external with: `/users?external=true` +You can search users by creation date time range with: + +``` +GET /users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060 +``` + ## Single user Get a single user. diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md index 0239e6b3163..72a34aa7de9 100644 --- a/doc/development/background_migrations.md +++ b/doc/development/background_migrations.md @@ -50,14 +50,13 @@ your migration: BackgroundMigrationWorker.perform_async('BackgroundMigrationClassName', [arg1, arg2, ...]) ``` -Usually it's better to schedule jobs in bulk, for this you can use +Usually it's better to enqueue jobs in bulk, for this you can use `BackgroundMigrationWorker.perform_bulk`: ```ruby BackgroundMigrationWorker.perform_bulk( - ['BackgroundMigrationClassName', [1]], - ['BackgroundMigrationClassName', [2]], - ... + [['BackgroundMigrationClassName', [1]], + ['BackgroundMigrationClassName', [2]]] ) ``` @@ -68,6 +67,16 @@ consuming migrations it's best to schedule a background job using an updates. Removals in turn can be handled by simply defining foreign keys with cascading deletes. +If you would like to schedule jobs in bulk with a delay, you can use +`BackgroundMigrationWorker.perform_bulk_in`: + +```ruby +jobs = [['BackgroundMigrationClassName', [1]], + ['BackgroundMigrationClassName', [2]]] + +BackgroundMigrationWorker.perform_bulk_in(5.minutes, jobs) +``` + ## Cleaning Up Because background migrations can take a long time you can't immediately clean diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index 5e9cf5e68b1..ecb79317093 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -1,6 +1,11 @@ module API module Helpers module InternalHelpers + SSH_GITALY_FEATURES = { + 'git-receive-pack' => :ssh_receive_pack, + 'git-upload-pack' => :ssh_upload_pack + }.freeze + def wiki? set_project unless defined?(@wiki) @wiki @@ -10,7 +15,7 @@ module API set_project unless defined?(@project) @project end - + def redirected_path @redirected_path end @@ -54,15 +59,33 @@ module API Gitlab::GlRepository.gl_repository(project, wiki?) end - # Return the repository full path so that gitlab-shell has it when - # handling ssh commands - def repository_path + # Return the repository depending on whether we want the wiki or the + # regular repository + def repository if wiki? - project.wiki.repository.path_to_repo + project.wiki.repository else - project.repository.path_to_repo + project.repository end end + + # Return the repository full path so that gitlab-shell has it when + # handling ssh commands + def repository_path + repository.path_to_repo + end + + # Return the Gitaly Address if it is enabled + def gitaly_payload(action) + feature = SSH_GITALY_FEATURES[action] + return unless feature && Gitlab::GitalyClient.feature_enabled?(feature) + + { + repository: repository.gitaly_repository, + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + } + end end end end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index f1c79970ba4..ef2c08e902c 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -47,7 +47,8 @@ module API { status: true, gl_repository: gl_repository, - repository_path: repository_path + repository_path: repository_path, + gitaly: gitaly_payload(params[:action]) } end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 1118fc7465b..d419d345ec5 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -41,7 +41,9 @@ module API args[:milestone_title] = args.delete(:milestone) args[:label_name] = args.delete(:labels) - merge_requests = MergeRequestsFinder.new(current_user, args).execute.inc_notes_with_associations + merge_requests = MergeRequestsFinder.new(current_user, args).execute + .inc_notes_with_associations + .preload(:target_project, :author, :assignee, :milestone, :merge_request_diff) merge_requests.reorder(args[:order_by] => args[:sort]) end diff --git a/lib/api/users.rb b/lib/api/users.rb index 88bca235692..c469751c31c 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -48,6 +48,8 @@ module API optional :active, type: Boolean, default: false, desc: 'Filters only active users' optional :external, type: Boolean, default: false, desc: 'Filters only external users' optional :blocked, type: Boolean, default: false, desc: 'Filters only blocked users' + optional :created_after, type: DateTime, desc: 'Return users created after the specified time' + optional :created_before, type: DateTime, desc: 'Return users created before the specified time' all_or_none_of :extern_uid, :provider use :pagination @@ -55,6 +57,10 @@ module API get do authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) + unless current_user&.admin? + params.except!(:created_after, :created_before) + end + users = UsersFinder.new(current_user, params).execute authorized = can?(current_user, :read_users_list) diff --git a/lib/gitlab/background_migration/migrate_build_stage_id_reference.rb b/lib/gitlab/background_migration/migrate_build_stage_id_reference.rb new file mode 100644 index 00000000000..91540127ea9 --- /dev/null +++ b/lib/gitlab/background_migration/migrate_build_stage_id_reference.rb @@ -0,0 +1,19 @@ +module Gitlab + module BackgroundMigration + class MigrateBuildStageIdReference + def perform(start_id, stop_id) + sql = <<-SQL.strip_heredoc + UPDATE ci_builds + SET stage_id = + (SELECT id FROM ci_stages + WHERE ci_stages.pipeline_id = ci_builds.commit_id + AND ci_stages.name = ci_builds.stage) + WHERE ci_builds.id BETWEEN #{start_id.to_i} AND #{stop_id.to_i} + AND ci_builds.stage_id IS NULL + SQL + + ActiveRecord::Base.connection.execute(sql) + end + end + end +end diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb index 780b309b45e..1bab6d64388 100644 --- a/spec/finders/users_finder_spec.rb +++ b/spec/finders/users_finder_spec.rb @@ -45,6 +45,17 @@ describe UsersFinder do expect(users).to contain_exactly(user, user1, user2, omniauth_user) end + + it 'filters by created_at' do + filtered_user_before = create(:user, created_at: 3.days.ago) + filtered_user_after = create(:user, created_at: Time.now + 3.days) + + users = described_class.new(user, + created_after: 2.days.ago, + created_before: Time.now + 2.days).execute + + expect(users.map(&:username)).not_to include([filtered_user_before.username, filtered_user_after.username]) + end end context 'with an admin user' do diff --git a/spec/migrations/migrate_stage_id_reference_in_background_spec.rb b/spec/migrations/migrate_stage_id_reference_in_background_spec.rb new file mode 100644 index 00000000000..260378adaa7 --- /dev/null +++ b/spec/migrations/migrate_stage_id_reference_in_background_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170628080858_migrate_stage_id_reference_in_background') + +describe MigrateStageIdReferenceInBackground, :migration, :sidekiq do + matcher :be_scheduled_migration do |delay, *expected| + match do |migration| + BackgroundMigrationWorker.jobs.any? do |job| + job['args'] == [migration, expected] && + job['at'].to_i == (delay.to_i + Time.now.to_i) + end + end + + failure_message do |migration| + "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" + end + end + + let(:jobs) { table(:ci_builds) } + let(:stages) { table(:ci_stages) } + let(:pipelines) { table(:ci_pipelines) } + let(:projects) { table(:projects) } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 3) + stub_const("#{described_class.name}::RANGE_SIZE", 2) + + projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 345, name: 'gitlab2', path: 'gitlab2') + + pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a') + pipelines.create!(id: 2, project_id: 345, ref: 'feature', sha: 'cdf43c3c') + + jobs.create!(id: 1, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build') + jobs.create!(id: 2, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build') + jobs.create!(id: 3, commit_id: 1, project_id: 123, stage_idx: 1, stage: 'test') + jobs.create!(id: 4, commit_id: 1, project_id: 123, stage_idx: 3, stage: 'deploy') + jobs.create!(id: 5, commit_id: 2, project_id: 345, stage_idx: 1, stage: 'test') + + stages.create(id: 101, pipeline_id: 1, project_id: 123, name: 'test') + stages.create(id: 102, pipeline_id: 1, project_id: 123, name: 'build') + stages.create(id: 103, pipeline_id: 1, project_id: 123, name: 'deploy') + + jobs.create!(id: 6, commit_id: 2, project_id: 345, stage_id: 101, stage_idx: 1, stage: 'test') + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_migration(2.minutes, 1, 2) + expect(described_class::MIGRATION).to be_scheduled_migration(2.minutes, 3, 3) + expect(described_class::MIGRATION).to be_scheduled_migration(4.minutes, 4, 5) + expect(BackgroundMigrationWorker.jobs.size).to eq 3 + end + end + end + + it 'schedules background migrations' do + Sidekiq::Testing.inline! do + expect(jobs.where(stage_id: nil).count).to eq 5 + + migrate! + + expect(jobs.where(stage_id: nil).count).to eq 1 + end + end +end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 6deaea956e0..cde4fa888a0 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -220,26 +220,72 @@ describe API::Internal do end context "git pull" do - it do - pull(key, project) + context "gitaly disabled" do + it "has the correct payload" do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:ssh_upload_pack).and_return(false) + pull(key, project) - expect(response).to have_http_status(200) - expect(json_response["status"]).to be_truthy - expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) - expect(json_response["gl_repository"]).to eq("project-#{project.id}") - expect(user).to have_an_activity_record + expect(response).to have_http_status(200) + expect(json_response["status"]).to be_truthy + expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) + expect(json_response["gl_repository"]).to eq("project-#{project.id}") + expect(json_response["gitaly"]).to be_nil + expect(user).to have_an_activity_record + end + end + + context "gitaly enabled" do + it "has the correct payload" do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:ssh_upload_pack).and_return(true) + pull(key, project) + + expect(response).to have_http_status(200) + expect(json_response["status"]).to be_truthy + expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) + expect(json_response["gl_repository"]).to eq("project-#{project.id}") + expect(json_response["gitaly"]).not_to be_nil + expect(json_response["gitaly"]["repository"]).not_to be_nil + expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) + expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path) + expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage)) + expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage)) + expect(user).to have_an_activity_record + end end end context "git push" do - it do - push(key, project) + context "gitaly disabled" do + it "has the correct payload" do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:ssh_receive_pack).and_return(false) + push(key, project) - expect(response).to have_http_status(200) - expect(json_response["status"]).to be_truthy - expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) - expect(json_response["gl_repository"]).to eq("project-#{project.id}") - expect(user).not_to have_an_activity_record + expect(response).to have_http_status(200) + expect(json_response["status"]).to be_truthy + expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) + expect(json_response["gl_repository"]).to eq("project-#{project.id}") + expect(json_response["gitaly"]).to be_nil + expect(user).not_to have_an_activity_record + end + end + + context "gitaly enabled" do + it "has the correct payload" do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:ssh_receive_pack).and_return(true) + push(key, project) + + expect(response).to have_http_status(200) + expect(json_response["status"]).to be_truthy + expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) + expect(json_response["gl_repository"]).to eq("project-#{project.id}") + expect(json_response["gitaly"]).not_to be_nil + expect(json_response["gitaly"]["repository"]).not_to be_nil + expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) + expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path) + expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage)) + expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage)) + expect(user).not_to have_an_activity_record + end end context 'project as /namespace/project' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 70b94a09e6b..c34b88f0741 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -163,6 +163,35 @@ describe API::Users do expect(response).to have_http_status(400) end + + it "returns a user created before a specific date" do + user = create(:user, created_at: Date.new(2000, 1, 1)) + + get api("/users?created_before=2000-01-02T00:00:00.060Z", admin) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(1) + expect(json_response.first['username']).to eq(user.username) + end + + it "returns no users created before a specific date" do + create(:user, created_at: Date.new(2001, 1, 1)) + + get api("/users?created_before=2000-01-02T00:00:00.060Z", admin) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(0) + end + + it "returns users created before and after a specific date" do + user = create(:user, created_at: Date.new(2001, 1, 1)) + + get api("/users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060", admin) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(1) + expect(json_response.first['username']).to eq(user.username) + end end end diff --git a/spec/support/sidekiq.rb b/spec/support/sidekiq.rb index 575d3451150..5478fea4e64 100644 --- a/spec/support/sidekiq.rb +++ b/spec/support/sidekiq.rb @@ -3,3 +3,9 @@ require 'sidekiq/testing/inline' Sidekiq::Testing.server_middleware do |chain| chain.add Gitlab::SidekiqStatus::ServerMiddleware end + +RSpec.configure do |config| + config.after(:each, :sidekiq) do + Sidekiq::Worker.clear_all + end +end diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb index 85939429feb..4f6e3474634 100644 --- a/spec/workers/background_migration_worker_spec.rb +++ b/spec/workers/background_migration_worker_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe BackgroundMigrationWorker do +describe BackgroundMigrationWorker, :sidekiq do describe '.perform' do it 'performs a background migration' do expect(Gitlab::BackgroundMigration) @@ -10,4 +10,35 @@ describe BackgroundMigrationWorker do described_class.new.perform('Foo', [10, 20]) end end + + describe '.perform_bulk' do + it 'enqueues background migrations in bulk' do + Sidekiq::Testing.fake! do + described_class.perform_bulk([['Foo', [1]], ['Foo', [2]]]) + + expect(described_class.jobs.count).to eq 2 + expect(described_class.jobs).to all(include('enqueued_at')) + end + end + end + + describe '.perform_bulk_in' do + context 'when delay is valid' do + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + described_class.perform_bulk_in(1.minute, [['Foo', [1]], ['Foo', [2]]]) + + expect(described_class.jobs.count).to eq 2 + expect(described_class.jobs).to all(include('at')) + end + end + end + + context 'when delay is invalid' do + it 'raises an ArgumentError exception' do + expect { described_class.perform_bulk_in(-60, [['Foo']]) } + .to raise_error(ArgumentError) + end + end + end end |