diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-07 03:11:11 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-07 03:11:11 +0300 |
commit | d4f8f25db649b973f1ae344cb0f8a407862d106b (patch) | |
tree | f71f2d2243dc768a1ec44e79556d8020bff51dc7 /app | |
parent | 5f0e3773e9695fd0c9e92ea9180c8a1f5cfaa5c5 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
21 files changed, 357 insertions, 51 deletions
diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js index 894185fa8c2..1226fbb670a 100644 --- a/app/assets/javascripts/pages/projects/pipelines/index/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js @@ -51,6 +51,7 @@ document.addEventListener( hasGitlabCi: parseBoolean(this.dataset.hasGitlabCi), ciLintPath: this.dataset.ciLintPath, resetCachePath: this.dataset.resetCachePath, + projectId: this.dataset.projectId, }, }); }, diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index d4f23697e09..ad78f5232ef 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -9,14 +9,18 @@ import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue'; import NavigationControls from './nav_controls.vue'; import { getParameterByName } from '../../lib/utils/common_utils'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; +import PipelinesFilteredSearch from './pipelines_filtered_search.vue'; +import { ANY_TRIGGER_AUTHOR } from '../constants'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { TablePagination, NavigationTabs, NavigationControls, + PipelinesFilteredSearch, }, - mixins: [pipelinesMixin, CIPaginationMixin], + mixins: [pipelinesMixin, CIPaginationMixin, glFeatureFlagsMixin()], props: { store: { type: Object, @@ -78,6 +82,10 @@ export default { required: false, default: null, }, + projectId: { + type: String, + required: true, + }, }, data() { return { @@ -209,6 +217,9 @@ export default { }, ]; }, + canFilterPipelines() { + return this.glFeatures.filterPipelinesSearch; + }, }, created() { this.service = new PipelinesService(this.endpoint); @@ -238,6 +249,19 @@ export default { createFlash(s__('Pipelines|Something went wrong while cleaning runners cache.')); }); }, + filterPipelines(filters) { + filters.forEach(filter => { + this.requestData[filter.type] = filter.value.data; + }); + + // set query params back to default if filtering by Any author + // or input is cleared on submit + if (this.requestData.username === ANY_TRIGGER_AUTHOR || filters.length === 0) { + this.requestData = { page: this.page, scope: this.scope }; + } + + this.updateContent(this.requestData); + }, }, }; </script> @@ -267,6 +291,13 @@ export default { /> </div> + <pipelines-filtered-search + v-if="canFilterPipelines" + :pipelines="state.pipelines" + :project-id="projectId" + @filterPipelines="filterPipelines" + /> + <div class="content-list pipelines"> <gl-loading-icon v-if="stateToRender === $options.stateMap.loading" diff --git a/app/assets/javascripts/pipelines/components/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_filtered_search.vue new file mode 100644 index 00000000000..178f08d84cc --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_filtered_search.vue @@ -0,0 +1,69 @@ +<script> +import { GlFilteredSearch } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue'; +import Api from '~/api'; +import createFlash from '~/flash'; + +export default { + components: { + GlFilteredSearch, + }, + props: { + pipelines: { + type: Array, + required: true, + }, + projectId: { + type: String, + required: true, + }, + }, + data() { + return { + projectUsers: null, + }; + }, + computed: { + tokens() { + return [ + { + type: 'username', + icon: 'user', + title: s__('Pipeline|Trigger author'), + dataType: 'username', + unique: true, + token: PipelineTriggerAuthorToken, + operators: [{ value: '=', description: __('is'), default: 'true' }], + triggerAuthors: this.projectUsers, + }, + ]; + }, + }, + created() { + Api.projectUsers(this.projectId) + .then(users => { + this.projectUsers = users; + }) + .catch(err => { + createFlash(__('There was a problem fetching project users.')); + throw err; + }); + }, + methods: { + onSubmit(filters) { + this.$emit('filterPipelines', filters); + }, + }, +}; +</script> + +<template> + <div class="row-content-block"> + <gl-filtered-search + :placeholder="__('Filter pipelines')" + :available-tokens="tokens" + @submit="onSubmit" + /> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/tokens/pipeline_trigger_author_token.vue b/app/assets/javascripts/pipelines/components/tokens/pipeline_trigger_author_token.vue new file mode 100644 index 00000000000..55b783024d3 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/tokens/pipeline_trigger_author_token.vue @@ -0,0 +1,78 @@ +<script> +import { + GlFilteredSearchToken, + GlAvatar, + GlFilteredSearchSuggestion, + GlDropdownDivider, +} from '@gitlab/ui'; +import { ANY_TRIGGER_AUTHOR } from '../../constants'; + +export default { + anyTriggerAuthor: ANY_TRIGGER_AUTHOR, + components: { + GlFilteredSearchToken, + GlAvatar, + GlFilteredSearchSuggestion, + GlDropdownDivider, + }, + props: { + config: { + type: Object, + required: true, + }, + value: { + type: Object, + required: true, + }, + }, + computed: { + currentValue() { + return this.value.data.toLowerCase(); + }, + filteredTriggerAuthors() { + return this.config.triggerAuthors.filter(user => { + return user.username.toLowerCase().includes(this.currentValue); + }); + }, + activeUser() { + return this.config.triggerAuthors.find(user => { + return user.username.toLowerCase() === this.currentValue; + }); + }, + }, +}; +</script> + +<template> + <gl-filtered-search-token :config="config" v-bind="{ ...$props, ...$attrs }" v-on="$listeners"> + <template #view="{inputValue}"> + <gl-avatar + v-if="activeUser" + :size="16" + :src="activeUser.avatar_url" + shape="circle" + class="gl-mr-2" + /> + <span>{{ activeUser ? activeUser.name : inputValue }}</span> + </template> + <template #suggestions> + <gl-filtered-search-suggestion :value="$options.anyTriggerAuthor">{{ + $options.anyTriggerAuthor + }}</gl-filtered-search-suggestion> + <gl-dropdown-divider /> + <gl-filtered-search-suggestion + v-for="user in filteredTriggerAuthors" + :key="user.username" + :value="user.username" + > + <div class="d-flex"> + <gl-avatar :size="32" :src="user.avatar_url" /> + <div> + <div>{{ user.name }}</div> + <div>@{{ user.username }}</div> + </div> + </div> + </gl-filtered-search-suggestion> + </template> + </gl-filtered-search-token> +</template> diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index c9655d18a04..d334e867101 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -1,6 +1,7 @@ export const CANCEL_REQUEST = 'CANCEL_REQUEST'; export const PIPELINES_TABLE = 'PIPELINES_TABLE'; export const LAYOUT_CHANGE_DELAY = 300; +export const ANY_TRIGGER_AUTHOR = 'Any'; export const TestStatus = { FAILED: 'failed', diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js index 3c755db23dc..82d082a226f 100644 --- a/app/assets/javascripts/pipelines/services/pipelines_service.js +++ b/app/assets/javascripts/pipelines/services/pipelines_service.js @@ -19,13 +19,19 @@ export default class PipelinesService { } getPipelines(data = {}) { - const { scope, page } = data; + const { scope, page, username } = data; const { CancelToken } = axios; + const queryParams = { scope, page }; + + if (username) { + queryParams.username = username; + } + this.cancelationSource = CancelToken.source(); return axios.get(this.endpoint, { - params: { scope, page }, + params: queryParams, cancelToken: this.cancelationSource.token, }); } diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js index f9e3f3df0cc..69e9839380f 100644 --- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js +++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js @@ -9,21 +9,41 @@ import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/c export default { methods: { onChangeTab(scope) { - this.updateContent({ scope, page: '1' }); + let params = { + scope, + page: '1', + }; + + params = this.onChangeWithFilter(params); + + this.updateContent(params); }, onChangePage(page) { /* URLS parameters are strings, we need to parse to match types */ - const params = { + let params = { page: Number(page).toString(), }; if (this.scope) { params.scope = this.scope; } + + params = this.onChangeWithFilter(params); + this.updateContent(params); }, + onChangeWithFilter(params) { + const { username } = this.requestData; + + if (username) { + return { ...params, username }; + } + + return params; + }, + updateInternalState(parameters) { // stop polling this.poll.stop(); diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index c578925e04c..a7b52121d2b 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -24,9 +24,8 @@ class Projects::PipelinesController < Projects::ApplicationController POLLING_INTERVAL = 10_000 def index - @scope = params[:scope] @pipelines = Ci::PipelinesFinder - .new(project, current_user, scope: @scope) + .new(project, current_user, index_params) .execute .page(params[:page]) .per(30) @@ -256,7 +255,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def limited_pipelines_count(project, scope = nil) - finder = Ci::PipelinesFinder.new(project, current_user, scope: scope) + finder = Ci::PipelinesFinder.new(project, current_user, index_params.merge(scope: scope)) view_context.limited_counter_with_delimiter(finder.execute) end @@ -268,6 +267,10 @@ class Projects::PipelinesController < Projects::ApplicationController end end end + + def index_params + params.permit(:scope, :username) + end end Projects::PipelinesController.prepend_if_ee('EE::Projects::PipelinesController') diff --git a/app/finders/releases_finder.rb b/app/finders/releases_finder.rb index e58a90922a5..6a754fdb5a1 100644 --- a/app/finders/releases_finder.rb +++ b/app/finders/releases_finder.rb @@ -1,17 +1,31 @@ # frozen_string_literal: true class ReleasesFinder - def initialize(project, current_user = nil) + attr_reader :project, :current_user, :params + + def initialize(project, current_user = nil, params = {}) @project = project @current_user = current_user + @params = params end def execute(preload: true) - return Release.none unless Ability.allowed?(@current_user, :read_release, @project) + return Release.none unless Ability.allowed?(current_user, :read_release, project) # See https://gitlab.com/gitlab-org/gitlab/-/issues/211988 - releases = @project.releases.where.not(tag: nil) # rubocop:disable CodeReuse/ActiveRecord + releases = project.releases.where.not(tag: nil) # rubocop:disable CodeReuse/ActiveRecord + releases = by_tag(releases) releases = releases.preloaded if preload releases.sorted end + + private + + # rubocop: disable CodeReuse/ActiveRecord + def by_tag(releases) + return releases unless params[:tag].present? + + releases.where(tag: params[:tag]) + end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/graphql/resolvers/release_resolver.rb b/app/graphql/resolvers/release_resolver.rb new file mode 100644 index 00000000000..9bae8b8cd13 --- /dev/null +++ b/app/graphql/resolvers/release_resolver.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Resolvers + class ReleaseResolver < BaseResolver + type Types::ReleaseType, null: true + + argument :tag_name, GraphQL::STRING_TYPE, + required: true, + description: 'The name of the tag associated to the release' + + alias_method :project, :object + + def self.single + self + end + + def resolve(tag_name:) + ReleasesFinder.new( + project, + current_user, + { tag: tag_name } + ).execute.first + end + end +end diff --git a/app/graphql/resolvers/releases_resolver.rb b/app/graphql/resolvers/releases_resolver.rb new file mode 100644 index 00000000000..b2afbb92684 --- /dev/null +++ b/app/graphql/resolvers/releases_resolver.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Resolvers + class ReleasesResolver < BaseResolver + type Types::ReleaseType.connection_type, null: true + + alias_method :project, :object + + # This resolver has a custom singular resolver + def self.single + Resolvers::ReleaseResolver + end + + def resolve(**args) + ReleasesFinder.new( + project, + current_user + ).execute + end + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index ebde6c4c98d..e7a83446610 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -217,6 +217,20 @@ module Types null: true, description: 'A single Alert Management alert of the project', resolver: Resolvers::AlertManagementAlertResolver.single + + field :releases, + Types::ReleaseType.connection_type, + null: true, + description: 'Releases of the project', + resolver: Resolvers::ReleasesResolver, + feature_flag: :graphql_release_data + + field :release, + Types::ReleaseType, + null: true, + description: 'A single release of the project', + resolver: Resolvers::ReleasesResolver.single, + feature_flag: :graphql_release_data end end diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb new file mode 100644 index 00000000000..26b47c191ca --- /dev/null +++ b/app/graphql/types/release_type.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Types + class ReleaseType < BaseObject + graphql_name 'Release' + + authorize :read_release + + alias_method :release, :object + + present_using ReleasePresenter + + field :tag_name, GraphQL::STRING_TYPE, null: false, method: :tag, + description: 'Name of the tag associated with the release' + field :tag_path, GraphQL::STRING_TYPE, null: true, + description: 'Relative web path to the tag associated with the release' + field :description, GraphQL::STRING_TYPE, null: true, + description: 'Description (also known as "release notes") of the release' + markdown_field :description_html, null: true + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the release' + field :evidence_sha, GraphQL::STRING_TYPE, null: true, + description: "SHA of the release's evidence" + field :created_at, Types::TimeType, null: true, + description: 'Timestamp of when the release was created' + field :released_at, Types::TimeType, null: true, + description: 'Timestamp of when the release was released' + field :milestones, Types::MilestoneType.connection_type, null: true, + description: 'Milestones associated to the release' + + field :author, Types::UserType, null: true, + description: 'User that created the release' + + def author + Gitlab::Graphql::Loaders::BatchModelLoader.new(User, release.author_id).find + end + + field :commit, Types::CommitType, null: true, + complexity: 10, calls_gitaly: true, + description: 'The commit associated with the release', + authorize: :reporter_access + + def commit + return if release.sha.nil? + + release.project.commit_by(oid: release.sha) + end + end +end diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index ec60c62794a..dcdf6bbd3da 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -80,7 +80,7 @@ module ServicesHelper def scoped_edit_integration_path(integration) if @project.present? - edit_project_settings_integration_path(@project, integration) + edit_project_service_path(@project, integration) elsif @group.present? edit_group_settings_integration_path(@group, integration) else diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb index 0815e27850d..40203ad692d 100644 --- a/app/models/project_services/youtrack_service.rb +++ b/app/models/project_services/youtrack_service.rb @@ -27,8 +27,8 @@ class YoutrackService < IssueTrackerService def fields [ { type: 'text', name: 'description', placeholder: description }, - { type: 'text', name: 'project_url', placeholder: 'Project url', required: true }, - { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true } + { type: 'text', name: 'project_url', title: 'Project URL', placeholder: 'Project URL', required: true }, + { type: 'text', name: 'issues_url', title: 'Issue URL', placeholder: 'Issue URL', required: true } ] end end diff --git a/app/views/admin/application_settings/integrations.html.haml b/app/views/admin/application_settings/integrations.html.haml index 2b01160a230..a8eff26b94c 100644 --- a/app/views/admin/application_settings/integrations.html.haml +++ b/app/views/admin/application_settings/integrations.html.haml @@ -18,7 +18,7 @@ %p = s_('AdminSettings|Integrations configured here will automatically apply to all projects on this instance.') = link_to _('Learn more'), '#' - = render 'shared/integrations/integrations', integrations: @integrations + = render 'shared/integrations/index', integrations: @integrations - else = render_if_exists 'admin/application_settings/elasticsearch_form' diff --git a/app/views/groups/settings/integrations/index.html.haml b/app/views/groups/settings/integrations/index.html.haml index 78825cc72b0..96bd6d69a96 100644 --- a/app/views/groups/settings/integrations/index.html.haml +++ b/app/views/groups/settings/integrations/index.html.haml @@ -6,4 +6,4 @@ %p = s_('GroupSettings|Integrations configured here will automatically apply to all projects in this group.') = link_to _('Learn more'), '#' -= render 'shared/integrations/integrations', integrations: @integrations += render 'shared/integrations/index', integrations: @integrations diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index f64f07487fd..64789c7c263 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -3,6 +3,7 @@ = render_if_exists "shared/shared_runners_minutes_limit_flash_message" #pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json), + project_id: @project.id, "help-page-path" => help_page_path('ci/quick_start/README'), "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml deleted file mode 100644 index dca324ac846..00000000000 --- a/app/views/projects/services/_index.html.haml +++ /dev/null @@ -1,30 +0,0 @@ -.row.prepend-top-default - .col-lg-4 - %h4.prepend-top-0 - = _('Integrations') - %p= _('Integrations allow you to integrate GitLab with other applications') - .col-lg-8 - %table.table - %colgroup - %col - %col - %col - %col{ width: "120" } - %thead - %tr - %th - %th= _('Integration') - %th.d-none.d-sm-block= _("Description") - %th= s_("ProjectService|Last edit") - - @services.sort_by(&:title).each do |service| - %tr - %td{ "aria-label" => (service.activated? ? s_("ProjectService|%{service_title}: status on") : s_("ProjectService|%{service_title}: status off")) % { service_title: service.title } } - = boolean_to_icon service.activated? - %td - = link_to edit_project_service_path(@project, service.to_param), { data: { qa_selector: "#{service.title.downcase.gsub(/[\s\(\)]/,'_')}_link" } } do - %strong= service.title - %td.d-none.d-sm-block - = service.description - %td.light - - if service.updated_at.present? - = time_ago_with_tooltip service.updated_at diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml index f603f23a2c7..4372763fcf7 100644 --- a/app/views/projects/settings/integrations/show.html.haml +++ b/app/views/projects/settings/integrations/show.html.haml @@ -12,4 +12,6 @@ .gl-alert-actions = link_to _('Go to Webhooks'), project_hooks_path(@project), class: 'btn gl-alert-action btn-info new-gl-button' -= render 'projects/services/index' +%h4= s_('Integrations') +%p= s_('Integrations allow you to integrate GitLab with other applications') += render 'shared/integrations/index', integrations: @services diff --git a/app/views/shared/integrations/_integrations.html.haml b/app/views/shared/integrations/_index.html.haml index b2359aca016..176f899d677 100644 --- a/app/views/shared/integrations/_integrations.html.haml +++ b/app/views/shared/integrations/_index.html.haml @@ -3,7 +3,7 @@ %col %col %col.d-none.d-sm-table-column - %col{ width: 120 } + %col{ width: 130 } %thead{ role: 'rowgroup' } %tr{ role: 'row' } %th{ role: 'columnheader', scope: 'col', 'aria-colindex': 1 } @@ -13,13 +13,14 @@ %tbody{ role: 'rowgroup' } - integrations.each do |integration| + - activated_label = (integration.activated? ? s_("ProjectService|%{service_title}: status on") : s_("ProjectService|%{service_title}: status off")) % { service_title: integration.title } %tr{ role: 'row' } - %td{ role: 'cell', 'aria-colindex': 1 } + %td{ role: 'cell', 'aria-colindex': 1, 'aria-label': activated_label } = boolean_to_icon integration.activated? %td{ role: 'cell', 'aria-colindex': 2 } - = link_to scoped_edit_integration_path(integration) do + = link_to scoped_edit_integration_path(integration), { data: { qa_selector: "#{integration.to_param}_link" } } do %strong= integration.title - %td.d-none.d-sm-block{ role: 'cell', 'aria-colindex': 3 } + %td.d-none.d-sm-table-cell{ role: 'cell', 'aria-colindex': 3 } = integration.description %td{ role: 'cell', 'aria-colindex': 4 } - if integration.updated_at.present? |