Commit e5398fc5 authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch 'master' into renovate/gitlab-package

parents bb4b8905 97991630
......@@ -17,7 +17,7 @@ stages:
# in cases where jobs require Docker-in-Docker, the job
# definition must be extended with `.use-docker-in-docker`
default:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.34"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2.patched-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.34"
tags:
- gitlab-org
# All jobs are interruptible by default
......
......@@ -272,3 +272,6 @@ Dangerfile @gl-quality/eng-prod
/lib/gitlab/experimentation/ @gitlab-org/growth/experiment-devs
/lib/gitlab/experimentation.rb @gitlab-org/growth/experiment-devs
/lib/gitlab/experimentation_logger.rb @gitlab-org/growth/experiment-devs
[Legal]
/config/dependency_decisions.yml @gitlab-org/legal-reviewers
cloud-native-image:
extends: .cng:rules
image: ruby:2.7-alpine
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine
dependencies: []
stage: post-test
variables:
......
......@@ -2,7 +2,7 @@
extends:
- .default-retry
- .docs:rules:review-docs
image: ruby:2.7-alpine
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine
stage: review
needs: []
variables:
......
......@@ -259,13 +259,13 @@ coverage-frontend:
qa-frontend-node:10:
extends: .qa-frontend-node
image: node:dubnium
image: ${GITLAB_DEPENDENCY_PROXY}node:dubnium
qa-frontend-node:latest:
extends:
- .qa-frontend-node
- .frontend:rules:qa-frontend-node-latest
image: node:latest
image: ${GITLAB_DEPENDENCY_PROXY}node:latest
webpack-dev-server:
extends:
......
......@@ -79,7 +79,7 @@
policy: pull
.use-pg11:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.34"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2.patched-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.34"
services:
- name: postgres:11.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
......@@ -88,7 +88,7 @@
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg12:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-12-graphicsmagick-1.3.34"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2.patched-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-12-graphicsmagick-1.3.34"
services:
- name: postgres:12
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
......@@ -97,7 +97,7 @@
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg11-ee:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.34"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2.patched-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.34"
services:
- name: postgres:11.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
......@@ -108,7 +108,7 @@
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg12-ee:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-12-graphicsmagick-1.3.34"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2.patched-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-12-graphicsmagick-1.3.34"
services:
- name: postgres:12
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
......@@ -132,7 +132,7 @@
FOSS_ONLY: '1'
.use-docker-in-docker:
image: docker:${DOCKER_VERSION}
image: ${GITLAB_DEPENDENCY_PROXY}docker:${DOCKER_VERSION}
services:
- docker:${DOCKER_VERSION}-dind
variables:
......
.notify-slack:
image: alpine
image: ${GITLAB_DEPENDENCY_PROXY}alpine
stage: notify
dependencies: []
cache: {}
......
......@@ -47,7 +47,7 @@ update-qa-cache:
policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up.
.package-and-qa-base:
image: ruby:2.7-alpine
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine
stage: qa
retry: 0
script:
......
......@@ -502,7 +502,6 @@ rspec:feature-flags:
- .coverage-base
- .rails:rules:rspec-feature-flags
stage: post-test
allow_failure: true
# We cannot use needs since it would mean needing 84 jobs (since most are parallelized)
# so we use `dependencies` here.
dependencies:
......@@ -522,7 +521,11 @@ rspec:feature-flags:
- memory-on-boot
script:
- run_timed_command "bundle install --jobs=$(nproc) --path=vendor --retry=3 --quiet --without default development test production puma unicorn kerberos metrics omnibus ed25519"
- 'run_timed_command "bundle exec scripts/used-feature-flags" || (scripts/slack master-broken "☠️ \`${CI_JOB_NAME}\` failed! ☠️ See ${CI_JOB_URL}" ci_failing "GitLab Bot" && exit 1)'
- if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]; then
run_timed_command "bundle exec scripts/used-feature-flags" || (scripts/slack master-broken "☠️ \`${CI_JOB_NAME}\` failed! ☠️ See ${CI_JOB_URL}" ci_failing "GitLab Bot" && exit 1);
else
run_timed_command "bundle exec scripts/used-feature-flags";
fi
# EE/FOSS: default refs (MRs, master, schedules) jobs #
#######################################################
......
......@@ -4,7 +4,7 @@
.merge-train-sync:
# We don't need/want any global before/after commands, so we overwrite these
# settings.
image: alpine:edge
image: ${GITLAB_DEPENDENCY_PROXY}alpine:edge
stage: sync
before_script:
- apk add --no-cache --update curl bash jq
......
......@@ -25,7 +25,7 @@ review-build-cng:
extends:
- .default-retry
- .review:rules:review-build-cng
image: ruby:2.7-alpine
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine
stage: review-prepare
before_script:
- source ./scripts/utils.sh
......@@ -199,7 +199,7 @@ review-performance:
parallel-spec-reports:
extends:
- .review:rules:mr-only-manual
image: ruby:2.7-alpine
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine
stage: post-qa
dependencies: ["review-qa-all"]
variables:
......
......@@ -879,6 +879,7 @@
- <<: *if-not-ee
when: never
- <<: *if-master-schedule-2-hourly
allow_failure: true
- <<: *if-merge-request-title-run-all-rspec
.rails:rules:master-schedule-nightly--code-backstage:
......
......@@ -26,7 +26,7 @@ cache gems:
dont-interrupt-me:
extends: .setup:rules:dont-interrupt-me
stage: sync
image: alpine:edge
image: ${GITLAB_DEPENDENCY_PROXY}alpine:edge
interruptible: false
variables:
GIT_STRATEGY: none
......@@ -52,7 +52,7 @@ no_ee_check:
verify-tests-yml:
extends:
- .setup:rules:verify-tests-yml
image: ruby:2.7-alpine
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine
stage: test
needs: []
script:
......@@ -61,7 +61,7 @@ verify-tests-yml:
- scripts/verify-tff-mapping
.detect-test-base:
image: ruby:2.7
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7
needs: []
stage: prepare
script:
......
.tests-metadata-state:
image: ruby:2.7
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7
before_script:
- source scripts/utils.sh
artifacts:
......
workhorse:
extends: .workhorse:rules:workhorse
image: golang:1.14
image: ${GITLAB_DEPENDENCY_PROXY}golang:1.14
stage: test
needs: []
script:
......
......@@ -111,5 +111,5 @@ Use the following resources to find the appropriate labels:
- https://about.gitlab.com/handbook/product/categories/features/
-->
/label ~devops:: ~group: ~Category:
/label ~"GitLab Core"/~"GitLab Premium"/~"GitLab Ultimate"
/label ~feature
......@@ -14,7 +14,7 @@
/label ~"feature" ~"group::" ~"section::" ~"Category::" ~"GitLab Core"/~"GitLab Starter"/~"GitLab Premium"/~"GitLab Ultimate"
/label ~"feature" ~"group::" ~"section::" ~"Category::" ~"GitLab Core"/~"GitLab Premium"/~"GitLab Ultimate"
<!--- Use the following resources to find the appropriate labels:
......
......@@ -2647,113 +2647,6 @@ Style/FrozenStringLiteralComment:
- 'app/views/projects/tags/index.atom.builder'
- 'app/views/users/show.atom.builder'
- 'bin/secpick'
- 'config.ru'
- 'config/boot.rb'
- 'config/environment.rb'
- 'config/environments/development.rb'
- 'config/environments/production.rb'
- 'config/environments/test.rb'
- 'config/initializers/01_secret_token.rb'
- 'config/initializers/0_acts_as_taggable.rb'
- 'config/initializers/0_inject_feature_flags.rb'
- 'config/initializers/0_post_deployment_migrations.rb'
- 'config/initializers/1_settings.rb'
- 'config/initializers/2_gitlab.rb'
- 'config/initializers/5_backend.rb'
- 'config/initializers/6_validations.rb'
- 'config/initializers/7_prometheus_metrics.rb'
- 'config/initializers/7_redis.rb'
- 'config/initializers/8_devise.rb'
- 'config/initializers/8_gitaly.rb'
- 'config/initializers/9_fast_gettext.rb'
- 'config/initializers/action_dispatch_http_mime_negotiation.rb'
- 'config/initializers/action_mailer_hooks.rb'
- 'config/initializers/active_record_data_types.rb'
- 'config/initializers/active_record_ping.rb'
- 'config/initializers/active_record_preloader.rb'
- 'config/initializers/active_record_schema_ignore_tables.rb'
- 'config/initializers/active_record_table_definition.rb'
- 'config/initializers/ar_speed_up_migration_checking.rb'
- 'config/initializers/asset_proxy_settings.rb'
- 'config/initializers/attr_encrypted_no_db_connection.rb'
- 'config/initializers/backtrace_silencers.rb'
- 'config/initializers/batch_loader.rb'
- 'config/initializers/bootstrap_form.rb'
- 'config/initializers/bullet.rb'
- 'config/initializers/cluster_events_before_phased_restart.rb'
- 'config/initializers/console_message.rb'
- 'config/initializers/cookies_serializer.rb'
- 'config/initializers/date_time_formats.rb'
- 'config/initializers/default_url_options.rb'
- 'config/initializers/deprecations.rb'
- 'config/initializers/direct_upload_support.rb'
- 'config/initializers/doorkeeper.rb'
- 'config/initializers/doorkeeper_openid_connect.rb'
- 'config/initializers/etag_caching.rb'
- 'config/initializers/fill_shards.rb'
- 'config/initializers/fix_local_cache_middleware.rb'
- 'config/initializers/fog_google_https_private_urls.rb'
- 'config/initializers/forbid_sidekiq_in_transactions.rb'
- 'config/initializers/gettext_rails_i18n_patch.rb'
- 'config/initializers/gitlab_kas_secret.rb'
- 'config/initializers/gitlab_shell_secret_token.rb'
- 'config/initializers/gitlab_workhorse_secret.rb'
- 'config/initializers/go_get.rb'
- 'config/initializers/grpc.rb'
- 'config/initializers/hamlit.rb'
- 'config/initializers/health_check.rb'
- 'config/initializers/http_hostname_override.rb'
- 'config/initializers/kaminari_active_record_relation_methods_with_limit.rb'
- 'config/initializers/kaminari_config.rb'
- 'config/initializers/lograge.rb'
- 'config/initializers/mail_encoding_patch.rb'
- 'config/initializers/mime_types.rb'
- 'config/initializers/mini_magick.rb'
- 'config/initializers/new_framework_defaults.rb'
- 'config/initializers/octokit.rb'
- 'config/initializers/omniauth.rb'
- 'config/initializers/peek.rb'
- 'config/initializers/postgresql_cte.rb'
- 'config/initializers/premailer.rb'
- 'config/initializers/query_limiting.rb'
- 'config/initializers/rack_lineprof.rb'
- 'config/initializers/relative_naming_ci_namespace.rb'
- 'config/initializers/request_context.rb'
- 'config/initializers/request_profiler.rb'
- 'config/initializers/routing_draw.rb'
- 'config/initializers/sentry.rb'
- 'config/initializers/server_uptime.rb'
- 'config/initializers/session_store.rb'
- 'config/initializers/sherlock.rb'
- 'config/initializers/sprockets.rb'
- 'config/initializers/static_files.rb'
- 'config/initializers/time_zone.rb'
- 'config/initializers/trusted_proxies.rb'
- 'config/initializers/warden.rb'
- 'config/initializers/workhorse_multipart.rb'
- 'config/initializers/wrap_parameters.rb'
- 'config/initializers/zz_metrics.rb'
- 'config/initializers_before_autoloader/000_inflections.rb'
- 'config/object_store_settings.rb'
- 'config/routes.rb'
- 'config/routes/admin.rb'
- 'config/routes/api.rb'
- 'config/routes/dashboard.rb'
- 'config/routes/development.rb'
- 'config/routes/explore.rb'
- 'config/routes/git_http.rb'
- 'config/routes/google_api.rb'
- 'config/routes/help.rb'
- 'config/routes/import.rb'
- 'config/routes/legacy_builds.rb'
- 'config/routes/repository.rb'
- 'config/routes/sherlock.rb'
- 'config/routes/sidekiq.rb'
- 'config/routes/snippets.rb'
- 'config/routes/uploads.rb'
- 'config/routes/wiki.rb'
- 'config/smime_signature_settings.rb'
- 'config/spring.rb'
- 'danger/changes_size/Dangerfile'
- 'danger/metadata/Dangerfile'
- 'db/migrate/20190325080727_truncate_user_fullname.rb'
......
9c2da9436f6a41a244a30deef6f48798f877e909
2c7c204731f6e4f1c8cdb3d8a705caf7acf6689d
......@@ -15,7 +15,7 @@ gem 'default_value_for', '~> 3.4.0'
# Supported DBs
gem 'pg', '~> 1.1'
gem 'rugged', '~> 0.28'
gem 'rugged', '~> 1.0.1'
gem 'grape-path-helpers', '~> 1.6.1'
gem 'faraday', '~> 1.0'
......@@ -266,7 +266,7 @@ gem 'babosa', '~> 1.0.2'
gem 'loofah', '~> 2.2'
# Working with license
gem 'licensee', '~> 8.9'
gem 'licensee', '~> 9.14.1'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.7'
......@@ -350,7 +350,7 @@ end
group :development, :test do
gem 'deprecation_toolkit', '~> 1.5.1', require: false
gem 'bullet', '~> 6.1.0'
gem 'bullet', '~> 6.1.3'
gem 'gitlab-pry-byebug', platform: :mri, require: ['pry-byebug', 'pry-byebug/pry_remote_ext']
gem 'pry-rails', '~> 0.3.9'
gem 'pry-remote'
......
......@@ -147,7 +147,7 @@ GEM
brakeman (4.2.1)
browser (4.2.0)
builder (3.2.4)
bullet (6.1.0)
bullet (6.1.3)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundler-audit (0.7.0.1)
......@@ -267,6 +267,7 @@ GEM
doorkeeper-openid_connect (1.7.5)
doorkeeper (>= 5.2, < 5.5)
json-jwt (>= 1.11.0)
dotenv (2.7.6)
dry-configurable (0.12.0)
concurrent-ruby (~> 1.0)
dry-core (~> 0.5, >= 0.5.0)
......@@ -674,8 +675,12 @@ GEM
toml (= 0.2.0)
with_env (= 1.1.0)
xml-simple
licensee (8.9.2)
rugged (~> 0.24)
licensee (9.14.1)
dotenv (~> 2.0)
octokit (~> 4.17)
reverse_markdown (~> 1.0)
rugged (>= 0.24, < 2.0)
thor (>= 0.19, < 2.0)
listen (3.2.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
......@@ -756,7 +761,7 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
octokit (4.15.0)
octokit (4.20.0)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
oj (3.10.6)
......@@ -990,6 +995,8 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
reverse_markdown (1.4.0)
nokogiri
rexml (3.2.4)
rinku (2.0.0)
rotp (2.1.2)
......@@ -1072,7 +1079,7 @@ GEM
rubyntlm (0.6.2)
rubypants (0.2.0)
rubyzip (2.0.0)
rugged (0.28.4.1)
rugged (1.0.1)
safe_yaml (1.0.4)
safety_net_attestation (0.4.0)
jwt (~> 2.0)
......@@ -1305,7 +1312,7 @@ DEPENDENCIES
bootstrap_form (~> 4.2.0)
brakeman (~> 4.2)
browser (~> 4.2)
bullet (~> 6.1.0)
bullet (~> 6.1.3)
bundler-audit (~> 0.7.0.1)
capybara (~> 3.34.0)
capybara-screenshot (~> 1.0.22)
......@@ -1417,7 +1424,7 @@ DEPENDENCIES
lefthook (~> 0.7)
letter_opener_web (~> 1.3.4)
license_finder (~> 6.0)
licensee (~> 8.9)
licensee (~> 9.14.1)
lockbox (~> 0.3.3)
lograge (~> 0.5)
loofah (~> 2.2)
......@@ -1501,7 +1508,7 @@ DEPENDENCIES
ruby-progressbar (~> 1.10)
ruby_parser (~> 3.15)
rubyzip (~> 2.0.0)
rugged (~> 0.28)
rugged (~> 1.0.1)
sanitize (~> 5.2.1)
sassc-rails (~> 2.1.0)
scss_lint (~> 0.59.0)
......
......@@ -86,16 +86,16 @@ export default {
return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
return (
this.listType === ListType.milestone &&
this.list.milestone &&
(!this.list.collapsed || !this.isSwimlanesHeader)
);
return this.listType === ListType.milestone && this.list.milestone && this.showListDetails;
},
showAssigneeListDetails() {
return (
this.listType === ListType.assignee && (!this.list.collapsed || !this.isSwimlanesHeader)
);
return this.listType === ListType.assignee && this.showListDetails;
},
showIterationListDetails() {
return this.listType === ListType.iteration && this.showListDetails;
},
showListDetails() {
return !this.list.collapsed || !this.isSwimlanesHeader;
},
issuesCount() {
return this.list.issuesCount;
......@@ -218,6 +218,17 @@ export default {
<gl-icon name="timer" />
</span>
<span
v-if="showIterationListDetails"
aria-hidden="true"
:class="{
'gl-mt-3 gl-rotate-90': list.collapsed,
'gl-mr-2': !list.collapsed,
}"
>
<gl-icon name="iteration" />
</span>
<a
v-if="showAssigneeListDetails"
:href="list.assignee.webUrl"
......
......@@ -78,14 +78,16 @@ export default {
return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
return (
this.list.type === 'milestone' &&
this.list.milestone &&
(this.list.isExpanded || !this.isSwimlanesHeader)
);
return this.list.type === 'milestone' && this.list.milestone && this.showListDetails;
},
showAssigneeListDetails() {
return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader);
return this.list.type === 'assignee' && this.showListDetails;
},
showIterationListDetails() {
return this.listType === ListType.iteration && this.showListDetails;
},
showListDetails() {
return this.list.isExpanded || !this.isSwimlanesHeader;
},
issuesCount() {
return this.list.issuesSize;
......@@ -203,6 +205,17 @@ export default {
<gl-icon name="timer" />
</span>
<span
v-if="showIterationListDetails"
aria-hidden="true"
:class="{
'gl-mt-3 gl-rotate-90': !list.isExpanded,
'gl-mr-2': list.isExpanded,
}"
>
<gl-icon name="iteration" />
</span>
<a
v-if="showAssigneeListDetails"
:href="list.assignee.path"
......
......@@ -5,17 +5,13 @@ import { __ } from '~/locale';
import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/sidebar/event_hub';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { LIST } from '~/boards/constants';
import { LIST, ListType, ListTypeTitles } from '~/boards/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
export default {
headerHeight: process.env.NODE_ENV === 'development' ? '75px' : '40px',
listSettingsText: __('List settings'),
assignee: 'assignee',
milestone: 'milestone',
label: 'label',
labelListText: __('Label'),
components: {
GlButton,
GlDrawer,
......@@ -33,6 +29,11 @@ export default {
default: false,
},
},
data() {
return {
ListType,
};
},
computed: {
...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
......@@ -56,7 +57,7 @@ export default {
return this.activeList.type || this.activeList.listType || null;
},
listTypeTitle() {
return this.$options.labelListText;
return ListTypeTitles[ListType.label];
},
showSidebar() {
return this.sidebarType === LIST;
......@@ -98,7 +99,7 @@ export default {
>
<template #header>{{ $options.listSettingsText }}</template>
<template v-if="isSidebarOpen">
<div v-if="boardListType === $options.label">
<div v-if="boardListType === ListType.label">
<label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
<gl-label
:title="activeListLabel.title"
......
import { __ } from '~/locale';
export const BoardType = {
project: 'project',
group: 'group',
......@@ -6,11 +8,19 @@ export const BoardType = {
export const ListType = {
assignee: 'assignee',
milestone: 'milestone',
iteration: 'iteration',
backlog: 'backlog',
closed: 'closed',
label: 'label',
};
export const ListTypeTitles = {
assignee: __('Assignee'),
milestone: __('Milestone'),
iteration: __('Iteration'),
label: __('Label'),
};
export const inactiveId = 0;
export const ISSUABLE = 'issuable';
......
export default class ListIteration {
constructor(obj) {
this.id = obj.id;
this.title = obj.title;
this.state = obj.state;
this.webUrl = obj.web_url || obj.webUrl;
this.description = obj.description;
}
}
......@@ -5,6 +5,7 @@ import boardsStore from '../stores/boards_store';
import ListLabel from './label';
import ListAssignee from './assignee';
import ListMilestone from './milestone';
import ListIteration from './iteration';
import 'ee_else_ce/boards/models/issue';
const TYPES = {
......@@ -57,6 +58,9 @@ class List {
} else if (IS_EE && obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
this.title = this.milestone.title;
} else if (IS_EE && obj.iteration) {
this.iteration = new ListIteration(obj.iteration);
this.title = this.iteration.title;
}
// doNotFetchIssues is a temporary workaround until issues are fetched using GraphQL on issue boards
......
import { __ } from '~/locale';
import { __, s__ } from '~/locale';
export const GENERIC_ERROR = __('Something went wrong on our end. Please try again!');
......@@ -9,8 +9,8 @@ export const DIFF_FILE_HEADER = {
};
export const DIFF_FILE = {
tooLarge: __('MRDiffFile|Changes are too large to be shown.'),
blobView: __('MRDiffFile|View file @ %{commitSha}'),
tooLarge: s__('MRDiffFile|Changes are too large to be shown.'),
blobView: s__('MRDiffFile|View file @ %{commitSha}'),
editInFork: __(
"You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
),
......
......@@ -159,7 +159,12 @@ export default {
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) {
const { latestDiff } = state;
const discussionLineCodes = [discussion.line_code, ...(discussion.line_codes || [])];
const originalStartLineCode = discussion.original_position?.line_range?.start?.line_code;
const discussionLineCodes = [
discussion.line_code,
originalStartLineCode,
...(discussion.line_codes || []),
];
const fileHash = discussion.diff_file.file_hash;
const lineCheck = (line) =>
discussionLineCodes.some(
......
......@@ -13,6 +13,9 @@ export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
export const EDITOR_READY_EVENT = 'editor-ready';
export const EDITOR_TYPE_CODE = 'vs.editor.ICodeEditor';
export const EDITOR_TYPE_DIFF = 'vs.editor.IDiffEditor';
//
// EXTENSIONS' CONSTANTS
//
......
......@@ -6,7 +6,12 @@ import { registerLanguages } from '~/ide/utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { uuids } from '~/diffs/utils/uuids';
import { clearDomElement } from './utils';
import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX, EDITOR_READY_EVENT } from './constants';
import {
EDITOR_LITE_INSTANCE_ERROR_NO_EL,
URI_PREFIX,
EDITOR_READY_EVENT,
EDITOR_TYPE_DIFF,
} from './constants';
export default class EditorLite {
constructor(options = {}) {
......@@ -29,15 +34,12 @@ export default class EditorLite {
monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
}
static updateModelLanguage(path, instance) {
if (!instance) return;
const model = instance.getModel();
static getModelLanguage(path) {
const ext = `.${path.split('.').pop()}`;
const language = monacoLanguages
.getLanguages()
.find((lang) => lang.extensions.indexOf(ext) !== -1);
const id = language ? language.id : 'plaintext';
monacoEditor.setModelLanguage(model, id);
return language ? language.id : 'plaintext';
}
static pushToImportsArray(arr, toImport) {
......@@ -102,17 +104,91 @@ export default class EditorLite {
});
}
static createEditorModel({ blobPath, blobContent, blobGlobalId, instance } = {}) {
let model = null;
static createEditorModel({
blobPath,
blobContent,
blobOriginalContent,
blobGlobalId,
instance,
isDiff,
} = {}) {
if (!instance) {
return null;
}
const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath);
const uri = Uri.file(uriFilePath);
const existingModel = monacoEditor.getModel(uri);
model = existingModel || monacoEditor.createModel(blobContent, undefined, uri);
instance.setModel(model);
return model;
const model = existingModel || monacoEditor.createModel(blobContent, undefined, uri);
if (!isDiff) {
instance.setModel(model);
return model;
}
const diffModel = {
original: monacoEditor.createModel(
blobOriginalContent,
EditorLite.getModelLanguage(model.uri.path),
),
modified: model,
};
instance.setModel(diffModel);
return diffModel;
}
static convertMonacoToELInstance = (inst) => {
const editorLiteInstanceAPI = {
updateModelLanguage: (path) => {
return EditorLite.instanceUpdateLanguage(inst, path);
},
use: (exts = []) => {
return EditorLite.instanceApplyExtension(inst, exts);
},
};
const handler = {
get(target, prop, receiver) {
if (Reflect.has(editorLiteInstanceAPI, prop)) {
return editorLiteInstanceAPI[prop];
}
return Reflect.get(target, prop, receiver);
},
};
return new Proxy(inst, handler);
};
static instanceUpdateLanguage(inst, path) {
const lang = EditorLite.getModelLanguage(path);
const model = inst.getModel();
return monacoEditor.setModelLanguage(model, lang);
}
static instanceApplyExtension(inst, exts = []) {
const extensions = [].concat(exts);
extensions.forEach((extension) => {
EditorLite.mixIntoInstance(extension, inst);
});
return inst;
}
static instanceRemoveFromRegistry(editor, instance) {
const index = editor.instances.findIndex((inst) => inst === instance);
editor.instances.splice(index, 1);
}
static instanceDisposeModels(editor, instance, model) {
const instanceModel = instance.getModel() || model;
if (!instanceModel) {
return;
}
if (instance.getEditorType() === EDITOR_TYPE_DIFF) {
const { original, modified } = instanceModel;
if (original) {
original.dispose();
}
if (modified) {
modified.dispose();
}
} else {
instanceModel.dispose();
}
}
/**
......@@ -128,26 +204,38 @@ export default class EditorLite {
el = undefined,
blobPath = '',
blobContent = '',
blobOriginalContent = '',
blobGlobalId = uuids()[0],
extensions = [],
isDiff = false,
...instanceOptions
} = {}) {
EditorLite.prepareInstance(el);
const instance = monacoEditor.create(el, {
...this.options,
...instanceOptions,
});
const createEditorFn = isDiff ? 'createDiffEditor' : 'create';
const instance = EditorLite.convertMonacoToELInstance(
monacoEditor[createEditorFn].call(this, el, {
...this.options,
...instanceOptions,
}),
);
const model = EditorLite.createEditorModel({ blobGlobalId, blobPath, blobContent, instance });
let model;
if (instanceOptions.model !== null) {
model = EditorLite.createEditorModel({
blobGlobalId,
blobOriginalContent,
blobPath,
blobContent,
instance,
isDiff,
});
}
instance.onDidDispose(() => {
const index = this.instances.findIndex((inst) => inst === instance);
this.instances.splice(index, 1);
model.dispose();
EditorLite.instanceRemoveFromRegistry(this, instance);
EditorLite.instanceDisposeModels(this, instance, model);
});
instance.updateModelLanguage = (path) => EditorLite.updateModelLanguage(path, instance);
instance.use = (args) => this.use(args, instance);
EditorLite.manageDefaultExtensions(instance, el, extensions);
......@@ -155,23 +243,21 @@ export default class EditorLite {
return instance;
}
createDiffInstance(args) {
return this.createInstance({
...args,
isDiff: true,
});
}
dispose() {
this.instances.forEach((instance) => instance.dispose());
}
use(exts = [], instance = null) {
const extensions = Array.isArray(exts) ? exts : [exts];
const initExtensions = (inst) => {
extensions.forEach((extension) => {
EditorLite.mixIntoInstance(extension, inst);
});
};
if (instance) {
initExtensions(instance);
} else {
this.instances.forEach((inst) => {
initExtensions(inst);
});
}
use(exts) {
this.instances.forEach((inst) => {
inst.use(exts);
});
return this;
}
}
<script>
/* eslint-disable vue/no-v-html */
import { GlLoadingIcon, GlBadge, GlTooltipDirective } from '@gitlab/ui';
import { showLearnGitLabGroupItemPopover } from '~/onboarding_issues';
import { visitUrl } from '../../lib/utils/url_utility';
import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub';
......@@ -77,11 +76,6 @@ export default {
return this.group.microdata || {};
},
},
mounted() {
if (this.group.name === 'Learn GitLab') {
showLearnGitLabGroupItemPopover(this.group.id);
}
},
methods: {
onClickRowGroup(e) {
const NO_EXPAND_CLS = 'no-expand';
......
......@@ -95,10 +95,7 @@ export default {
</script>
<template>
<div>
<!-- helpHtml is trusted input -->
<div v-if="helpHtml" v-safe-html:[$options.helpHtmlConfig]="helpHtml"></div>
<div class="gl-mb-3">
<override-dropdown
v-if="defaultState !== null"
:inherit-from-id="defaultState.id"
......@@ -107,80 +104,91 @@ export default {
@change="setOverride"
/>
<active-checkbox v-if="propsSource.showActive" :key="`${currentKey}-active-checkbox`" />
<jira-trigger-fields
v-if="isJira"
:key="`${currentKey}-jira-trigger-fields`"
v-bind="propsSource.triggerFieldsProps"
/>
<trigger-fields
v-else-if="propsSource.triggerEvents.length"
:key="`${currentKey}-trigger-fields`"
:events="propsSource.triggerEvents"
:type="propsSource.type"
/>
<dynamic-field
v-for="field in propsSource.fields"
:key="`${currentKey}-${field.name}`"
v-bind="field"
/>
<jira-issues-fields
v-if="showJiraIssuesFields"
:key="`${currentKey}-jira-issues-fields`"
v-bind="propsSource.jiraIssuesProps"
/>
<div v-if="isEditable" class="footer-block row-content-block">
<template v-if="isInstanceOrGroupLevel">
<gl-button
v-gl-modal.confirmSaveIntegration
category="primary"
variant="success"
:loading="isSaving"
:disabled="isDisabled"
data-qa-selector="save_changes_button"
>
{{ __('Save changes') }}
</gl-button>
<confirmation-modal @submit="onSaveClick" />
</template>
<gl-button
v-else
category="primary"
variant="success"
type="submit"
:loading="isSaving"
:disabled="isDisabled"
data-qa-selector="save_changes_button"
@click.prevent="onSaveClick"
>
{{ __('Save changes') }}
</gl-button>
<div class="row">
<div class="col-lg-4"></div>
<div class="col-lg-8">
<!-- helpHtml is trusted input -->
<div v-if="helpHtml" v-safe-html:[$options.helpHtmlConfig]="helpHtml"></div>
<active-checkbox v-if="propsSource.showActive" :key="`${currentKey}-active-checkbox`" />
<jira-trigger-fields
v-if="isJira"
:key="`${currentKey}-jira-trigger-fields`"
v-bind="propsSource.triggerFieldsProps"
/>
<trigger-fields
v-else-if="propsSource.triggerEvents.length"
:key="`${currentKey}-trigger-fields`"
:events="propsSource.triggerEvents"
:type="propsSource.type"
/>
<dynamic-field
v-for="field in propsSource.fields"
:key="`${currentKey}-${field.name}`"
v-bind="field"
/>
<jira-issues-fields
v-if="showJiraIssuesFields"
:key="`${currentKey}-jira-issues-fields`"
v-bind="propsSource.jiraIssuesProps"
/>
<div v-if="isEditable" class="footer-block row-content-block">
<template v-if="isInstanceOrGroupLevel">
<gl-button
v-gl-modal.confirmSaveIntegration
category="primary"
variant="success"
:loading="isSaving"
:disabled="isDisabled"
data-qa-selector="save_changes_button"
>
{{ __('Save changes') }}
</gl-button>
<confirmation-modal @submit="onSaveClick" />
</template>
<gl-button
v-else
category="primary"
variant="success"
type="submit"
:loading="isSaving"
:disabled="isDisabled"
data-qa-selector="save_changes_button"
@click.prevent="onSaveClick"
>
{{ __('Save changes') }}
</gl-button>
<gl-button
v-if="propsSource.canTest"
:loading="isTesting"
:disabled="isDisabled"
:href="propsSource.testPath"
@click.prevent="onTestClick"
>
{{ __('Test settings') }}
</gl-button>
<gl-button
v-if="propsSource.canTest"
:loading="isTesting"
:disabled="isDisabled"
:href="propsSource.testPath"
@click.prevent="onTestClick"
>
{{ __('Test settings') }}
</gl-button>
<template v-if="showReset">
<gl-button
v-gl-modal.confirmResetIntegration
category="secondary"
variant="default"
:loading="isResetting"
:disabled="isDisabled"
data-testid="reset-button"
>
{{ __('Reset') }}
</gl-button>
<reset-confirmation-modal @reset="onResetClick" />
</template>
<template v-if="showReset">
<gl-button
v-gl-modal.confirmResetIntegration
category="secondary"
variant="default"
:loading="isResetting"
:disabled="isDisabled"
data-testid="reset-button"
>
{{ __('Reset') }}
</gl-button>
<reset-confirmation-modal @reset="onResetClick" />
</template>
<gl-button class="btn-cancel" :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button>
<gl-button class="btn-cancel" :href="propsSource.cancelPath">{{
__('Cancel')
}}</gl-button>
</div>
</div>
</div>
</div>
</template>
......@@ -54,11 +54,6 @@ export default {
required: false,
default: null,
},
runnerHelpUrl: {
type: String,
required: false,
default: null,
},
deploymentHelpUrl: {
type: String,
required: false,
......@@ -250,7 +245,6 @@ export default {
v-if="shouldRenderSharedRunnerLimitWarning"
:quota-used="job.runners.quota.used"
:quota-limit="job.runners.quota.limit"
:runners-path="runnerHelpUrl"
:project-path="projectPath"
:subscriptions-more-minutes-url="subscriptionsMoreMinutesUrl"
/>
......@@ -330,7 +324,6 @@ export default {
'right-sidebar-collapsed': !isSidebarOpen,
}"
:artifact-help-url="artifactHelpUrl"
:runner-help-url="runnerHelpUrl"
data-testid="job-sidebar"
/>
</div>
......
<script>
import { GlBadge } from '@gitlab/ui';
export default {
components: {
GlBadge,
},
props: {
duration: {
type: String,
......@@ -9,7 +14,7 @@ export default {
};
</script>
<template>
<div class="log-duration-badge rounded align-self-start px-2 ml-2 flex-shrink-0 ws-normal">
<gl-badge>
{{ duration }}
</div>
</gl-badge>
</template>
......@@ -41,11 +41,6 @@ export default {
required: false,
default: '',
},
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapGetters(['hasForwardDeploymentFailure']),
......@@ -135,7 +130,7 @@ export default {
<gl-icon :size="14" name="external-link" />
</gl-link>
</div>
<job-sidebar-details-container :runner-help-url="runnerHelpUrl" />
<job-sidebar-details-container />
<artifacts-block v-if="hasArtifact" :artifact="job.artifact" :help-url="artifactHelpUrl" />
<trigger-block v-if="hasTriggers" :trigger="job.trigger" />
<commit-block
......
......@@ -3,6 +3,7 @@ import { mapState } from 'vuex';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { helpPagePath } from '~/helpers/help_page_helper';
import DetailRow from './sidebar_detail_row.vue';
export default {
......@@ -11,13 +12,6 @@ export default {
DetailRow,
},
mixins: [timeagoMixin],
props: {
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapState(['job']),
coverage() {
......@@ -51,6 +45,11 @@ export default {
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerHelpUrl() {
return helpPagePath('ci/runners/README.html', {
anchor: 'set-maximum-job-timeout-for-a-runner',
});
},
runnerId() {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
......
......@@ -13,7 +13,6 @@ export default () => {
const {
artifactHelpUrl,
deploymentHelpUrl,
runnerHelpUrl,
runnerSettingsUrl,
variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
......@@ -39,7 +38,6 @@ export default () => {
props: {
artifactHelpUrl,
deploymentHelpUrl,
runnerHelpUrl,
runnerSettingsUrl,
variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
......
......@@ -143,6 +143,9 @@ export default {
trackingLabel() {
return slugifyWithUnderscore(`${this.commentButtonTitle} button`);
},
hasCloseAndCommentButton() {
return !this.glFeatures.removeCommentCloseReopen;
},
},
watch: {
note(newNote) {
......@@ -405,7 +408,7 @@ export default {
</div>
<gl-button
v-if="canToggleIssueState"
v-if="hasCloseAndCommentButton && canToggleIssueState"
:loading="isToggleStateButtonLoading"
category="secondary"
:variant="buttonVariant"
......
import $ from 'jquery';
import { parseBoolean, getCookie, setCookie, removeCookie } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import Tracking from '~/tracking';
const COOKIE_NAME = 'onboarding_issues_settings';
const POPOVER_LOCATIONS = {
GROUPS_SHOW: 'groups#show',
PROJECTS_SHOW: 'projects#show',
ISSUES_INDEX: 'issues#index',
};
const removeLearnGitLabCookie = () => {
removeCookie(COOKIE_NAME);
};
function disposePopover(event) {
event.preventDefault();
this.popover('dispose');
removeLearnGitLabCookie();
Tracking.event('Growth::Conversion::Experiment::OnboardingIssues', 'dismiss_popover');
}
const showPopover = (el, path, footer, options) => {
// Cookie value looks like `{ 'groups#show': true, 'projects#show': true, 'issues#index': true }`. When it doesn't exist, don't show the popover.
const cookie = getCookie(COOKIE_NAME);
if (!cookie) return;
// When the popover action has already been taken, don't show the popover.
const settings = JSON.parse(cookie);
if (!parseBoolean(settings[path])) return;
const defaultOptions = {
boundary: 'window',
html: true,
placement: 'top',
template: `<div class="gl-popover popover blue learn-gitlab d-none d-xl-block" role="tooltip">
<div class="arrow"></div>
<div class="js-close-learn-gitlab gl-font-weight-bold gl-line-height-normal float-right gl-cursor-pointer gl-font-base gl-text-white gl-opacity-10 gl-p-3">&#10005</div>
<div class="popover-body gl-font-base"></div>
<div class="gl-font-weight-bold gl-text-right gl-text-white gl-p-3 gl-pt-0">${footer}</div>
</div>`,
};
// When one of the popovers is dismissed, remove the cookie.
const closeButton = () => document.querySelector('.js-close-learn-gitlab');
// We still have to use jQuery, since Bootstrap's Popover is based on jQuery.
const jQueryEl = $(el);
const clickCloseButton = disposePopover.bind(jQueryEl);
jQueryEl
.popover({ ...defaultOptions, ...options })
.on('inserted.bs.popover', () => closeButton().addEventListener('click', clickCloseButton))
.on('hide.bs.dropdown', () => closeButton().removeEventListener('click', clickCloseButton))
.popover('show');
// The previous popover actions have been taken, don't show those popovers anymore.
Object.keys(settings).forEach((pathSetting) => {
if (path !== pathSetting) {
settings[pathSetting] = false;
} else {
setCookie(COOKIE_NAME, settings);
}
});
// The final popover action will be taken on click, we then no longer need the cookie.
if (path === POPOVER_LOCATIONS.ISSUES_INDEX) {
el.addEventListener('click', removeLearnGitLabCookie);
}
};
export const showLearnGitLabGroupItemPopover = (id) => {
const el = document.querySelector(`#group-${id} .group-text a`);
if (!el) return;
const options = {
content: __(
'Here are all your projects in your group, including the one you just created. To start, let’s take a look at your personalized learning project which will help you learn about GitLab at your own pace.',
),
};
showPopover(el, POPOVER_LOCATIONS.GROUPS_SHOW, '1 / 2', options);
};
export const showLearnGitLabProjectPopover = () => {
// Do not show a popover if we are not viewing the 'Learn GitLab' project.
if (!window.location.pathname.includes('learn-gitlab')) return;
const el = document.querySelector('a.shortcuts-issues');
if (!el) return;
const options = {
content: sprintf(
__(
'Go to %{strongStart}Issues%{strongEnd} &gt; %{strongStart}Boards%{strongEnd} to access your personalized learning issue board.',
),
{ strongStart: '<strong>', strongEnd: '</strong>' },
false,
),
};
showPopover(el, POPOVER_LOCATIONS.PROJECTS_SHOW, '2 / 2', options);
};
export const showLearnGitLabIssuesPopover = () => {
// Do not show a popover if we are not viewing the 'Learn GitLab' project.
if (!window.location.pathname.includes('learn-gitlab')) return;
const el = document.querySelector('a[data-qa-selector="issue_boards_link"]');
if (!el) return;
const options = {
content: sprintf(
__(
'Go to %{strongStart}Issues%{strongEnd} &gt; %{strongStart}Boards%{strongEnd} to access your personalized learning issue board.',
),
{ strongStart: '<strong>', strongEnd: '</strong>' },
false,
),
};
showPopover(el, POPOVER_LOCATIONS.ISSUES_INDEX, '2 / 2', options);
};
......@@ -9,7 +9,6 @@ import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
import initIssuablesList from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
import { showLearnGitLabIssuesPopover } from '~/onboarding_issues';
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
......@@ -25,4 +24,3 @@ new UsersSelect();
initManualOrdering();
initIssuablesList();
showLearnGitLabIssuesPopover();
......@@ -7,7 +7,6 @@ import BlobViewer from '~/blob/viewer/index';
import Activities from '~/activities';
import initReadMore from '~/read_more';
import leaveByUrl from '~/namespaces/leave_by_url';
import { showLearnGitLabProjectPopover } from '~/onboarding_issues';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initVueNotificationsDropdown from '~/notifications';
......@@ -41,8 +40,6 @@ if (document.querySelector('.project-show-activity')) {
leaveByUrl('project');
showLearnGitLabProjectPopover();
if (gon.features?.vueNotificationDropdown) {
initVueNotificationsDropdown();
} else {
......
<script>
import CommitForm from './commit_form.vue';
import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
import { COMMIT_FAILURE, COMMIT_SUCCESS } from '../../constants';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';
export default {
alertTexts: {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
},
i18n: {
defaultCommitMessage: __('Update %{sourcePath} file'),
},
components: {
CommitForm,
},
inject: ['projectFullPath', 'ciConfigPath', 'defaultBranch', 'newMergeRequestPath'],
props: {
ciFileContent: {
type: String,
required: true,
},
},
data() {
return {
commit: {},
isSaving: false,
};
},
apollo: {
commitSha: {
query: getCommitSha,
},
},
computed: {
defaultCommitMessage() {
return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
},
},
methods: {
redirectToNewMergeRequest(sourceBranch) {
const url = mergeUrlParams(
{
[MR_SOURCE_BRANCH]: sourceBranch,
[MR_TARGET_BRANCH]: this.defaultBranch,
},
this.newMergeRequestPath,
);
redirectTo(url);
},
async onCommitSubmit({ message, branch, openMergeRequest }) {
this.isSaving = true;
try {
const {
data: {
commitCreate: { errors },
},
} = await this.$apollo.mutate({
mutation: commitCIFile,
variables: {
projectPath: this.projectFullPath,
branch,
startBranch: this.defaultBranch,
message,
filePath: this.ciConfigPath,
content: this.ciFileContent,
lastCommitId: this.commitSha,
},
update(store, { data }) {
const commitSha = data?.commitCreate?.commit?.sha;
if (commitSha) {
store.writeQuery({ query: getCommitSha, data: { commitSha } });
}
},
});
if (errors?.length) {
this.$emit('showError', { type: COMMIT_FAILURE, reasons: errors });
} else if (openMergeRequest) {
this.redirectToNewMergeRequest(branch);
} else {
this.$emit('commit', { type: COMMIT_SUCCESS });
}
} catch (error) {
this.$emit('showError', { type: COMMIT_FAILURE, reasons: [error?.message] });
} finally {
this.isSaving = false;
}
},
onCommitCancel() {
this.$emit('resetContent');
},
},
};
</script>
<template>
<commit-form
:default-branch="defaultBranch"
:default-message="defaultCommitMessage"
:is-saving="isSaving"
@cancel="onCommitCancel"
@submit="onCommitSubmit"
/>
</template>
<script>
import ValidationSegment from './validation_segment.vue';
export default {
validationSegmentClasses:
'gl-p-5 gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base',
components: {
ValidationSegment,
},
props: {
ciConfigData: {
type: Object,
required: true,
},
isCiConfigDataLoading: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<div class="gl-mb-5">
<validation-segment
:class="$options.validationSegmentClasses"
:loading="isCiConfigDataLoading"
:ci-config="ciConfigData"
/>
</div>
</template>
<script>
import { GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CiLint from './lint/ci_lint.vue';
import EditorTab from './ui/editor_tab.vue';
import TextEditor from './text_editor.vue';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
export default {
i18n: {
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
},
components: {
CiLint,
EditorTab,
GlLoadingIcon,
GlTab,
GlTabs,
PipelineGraph,
TextEditor,
},
mixins: [glFeatureFlagsMixin()],
props: {
ciConfigData: {
type: Object,
required: true,
},
ciFileContent: {
type: String,
required: true,
},
isCiConfigDataLoading: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<gl-tabs class="file-editor gl-mb-3">
<editor-tab :title="$options.i18n.tabEdit" lazy data-testid="editor-tab">
<text-editor :value="ciFileContent" v-on="$listeners" />
</editor-tab>
<gl-tab
v-if="glFeatures.ciConfigVisualizationTab"
:title="$options.i18n.tabGraph"
lazy
data-testid="visualization-tab"
>
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
<pipeline-graph v-else :pipeline-data="ciConfigData" />
</gl-tab>
<editor-tab :title="$options.i18n.tabLint" data-testid="lint-tab">
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
<ci-lint v-else :ci-config="ciConfigData" />
</editor-tab>
</gl-tabs>
</template>
......@@ -2,26 +2,29 @@
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
import { EDITOR_READY_EVENT } from '~/editor/constants';
import getCommitSha from '../graphql/queries/client/commit_sha.graphql';
export default {
components: {
EditorLite,
},
inject: ['projectPath', 'projectNamespace'],
inject: ['ciConfigPath', 'projectPath', 'projectNamespace'],
inheritAttrs: false,
props: {
ciConfigPath: {
type: String,
required: true,
},
data() {
return {
commitSha: '',
};
},
apollo: {
commitSha: {
type: String,
required: false,
default: null,
query: getCommitSha,
},
},
methods: {
onEditorReady() {
onCiConfigUpdate(content) {
this.$emit('updateCiConfig', content);
},
registerCiSchema() {
const editorInstance = this.$refs.editor.getEditor();
editorInstance.use(new CiSchemaExtension());
......@@ -41,7 +44,8 @@ export default {
ref="editor"
:file-name="ciConfigPath"
v-bind="$attrs"
@[$options.readyEvent]="onEditorReady"
@[$options.readyEvent]="registerCiSchema"
@input="onCiConfigUpdate"
v-on="$listeners"
/>
</div>
......
export const CI_CONFIG_STATUS_VALID = 'VALID';
export const CI_CONFIG_STATUS_INVALID = 'INVALID';
export const COMMIT_FAILURE = 'COMMIT_FAILURE';
export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
export const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
mutation commitCIFileMutation(
mutation commitCIFile(
$projectPath: ID!
$branch: String!
$startBranch: String
......
......@@ -15,14 +15,13 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
}
const {
// props
ciConfigPath,
// Add to apollo cache as it can be updated by future queries
commitSha,
// Add to provide/inject API for static values
ciConfigPath,
defaultBranch,
newMergeRequestPath,
// `provide/inject` data
lintHelpPagePath,
newMergeRequestPath,
projectFullPath,
projectPath,
projectNamespace,
......@@ -35,25 +34,27 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
defaultClient: createDefaultClient(resolvers, { typeDefs }),
});
apolloProvider.clients.defaultClient.cache.writeData({
data: {
commitSha,
},
});
return new Vue({
el,
apolloProvider,
provide: {
ciConfigPath,
defaultBranch,
lintHelpPagePath,
newMergeRequestPath,
projectFullPath,
projectPath,
projectNamespace,
ymlHelpPagePath,
},
render(h) {
return h(PipelineEditorApp, {
props: {
ciConfigPath,
commitSha,
defaultBranch,
newMergeRequestPath,
},
});
return h(PipelineEditorApp);
},
});
};
<script>
import CommitSection from './components/commit/commit_section.vue';
import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
export default {
components: {
CommitSection,
PipelineEditorHeader,
PipelineEditorTabs,
},
props: {
ciConfigData: {
type: Object,
required: true,
},
ciFileContent: {
type: String,
required: true,
},
isCiConfigDataLoading: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<div>
<pipeline-editor-header
:ci-config-data="ciConfigData"
:is-ci-config-data-loading="isCiConfigDataLoading"
/>
<pipeline-editor-tabs
:ci-config-data="ciConfigData"
:ci-file-content="ciFileContent"
:is-ci-config-data-loading="isCiConfigDataLoading"
v-on="$listeners"
/>
<commit-section :ci-file-content="ciFileContent" v-on="$listeners" />
</div>
</template>
......@@ -28,7 +28,6 @@ export default {
return {
currentPipeline: null,
loadingPipelineId: null,
minWidth: 0,
pipelineExpanded: false,
};
},
......@@ -40,6 +39,7 @@ export default {
'gl-pl-3',
'gl-mb-5',
],
minWidth: `${ONE_COL_WIDTH}px`,
computed: {
columnClass() {
const positionValues = {
......@@ -48,12 +48,6 @@ export default {
};
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
},
graphPosition() {
return this.isUpstream ? 'left' : 'right';
},
isUpstream() {
return this.type === UPSTREAM;
},
computedTitleClasses() {
const positionalClasses = this.isUpstream
? ['gl-w-full', 'gl-text-right', 'gl-linked-pipeline-padding']
......@@ -61,6 +55,12 @@ export default {
return [...this.$options.titleClasses, ...positionalClasses];
},
graphPosition() {
return this.isUpstream ? 'left' : 'right';
},
isUpstream() {
return this.type === UPSTREAM;
},
},
methods: {
getPipelineData(pipeline) {
......@@ -105,7 +105,6 @@ export default {
if (this.currentPipeline?.id === pipeline.id) {
this.pipelineExpanded = false;
this.currentPipeline = null;
this.minWidth = 0;
return;
}
......@@ -120,11 +119,6 @@ export default {
*/
this.pipelineExpanded = true;
/*
Min-width is set manually for timing reasons.
*/
this.minWidth = `${ONE_COL_WIDTH}px`;
this.getPipelineData(pipeline);
},
onDownstreamHovered(jobName) {
......@@ -138,6 +132,9 @@ export default {
this.$emit('pipelineExpandToggle', jobName, expanded);
},
showDownstreamContainer(id) {
return !this.isUpstream && (this.isExpanded(id) || this.isLoadingPipeline(id));
},
},
};
</script>
......@@ -167,12 +164,12 @@ export default {
@pipelineExpandToggle="onPipelineExpandToggle"
/>
<div
v-if="isExpanded(pipeline.id) && !isUpstream"
:style="{ minWidth }"
v-if="showDownstreamContainer(pipeline.id)"
:style="{ minWidth: $options.minWidth }"
class="gl-display-inline-block"
>
<pipeline-graph
v-if="currentPipeline"
v-if="isExpanded(pipeline.id)"
:type="type"
class="d-inline-block gl-mt-n2"
:pipeline="currentPipeline"
......
......@@ -74,13 +74,15 @@ export default {
<div v-else>
<gl-alert
v-if="showAlert"
class="gl-w-max-content gl-ml-4"
class="gl-ml-4 gl-mb-4"
:primary-button-text="$options.i18n.showLinksAnyways"
@primaryAction="overrideShowLinks"
@dismiss="dismissAlert"
>
{{ $options.i18n.tooManyJobs }}
</gl-alert>
<slot></slot>
<div class="gl-display-flex gl-relative">
<slot></slot>
</div>
</div>
</template>
......@@ -76,8 +76,8 @@ export default {
class="d-inline-block"
>
<!-- use d-flex so that slot can be appropriately styled -->
<span class="d-flex">
<reviewer-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
<span class="gl-display-flex gl-align-items-center">
<reviewer-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
<slot :user="user"></slot>
</span>
</gl-link>
......
......@@ -46,6 +46,9 @@ export default {
assignSelf() {
this.$emit('assign-self');
},
requestReview(data) {
this.$emit('request-review', data);
},
},
};
</script>
......@@ -66,6 +69,7 @@ export default {
:users="sortedReviewers"
:root-path="rootPath"
:issuable-type="issuableType"
@request-review="requestReview"
/>
</div>
</div>
......
......@@ -83,6 +83,9 @@ export default {
return new Flash(__('Error occurred when saving reviewers'));
});
},
requestReview(data) {
this.mediator.requestReview(data);
},
},
};
</script>
......@@ -101,6 +104,7 @@ export default {
:editable="store.editable"
:issuable-type="issuableType"
class="value"
@request-review="requestReview"
/>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
......@@ -8,8 +9,13 @@ const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
GlButton,
GlIcon,
ReviewerAvatarLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
users: {
type: Array,
......@@ -28,6 +34,8 @@ export default {
data() {
return {
showLess: true,
loading: false,
requestedReviewSuccess: false,
};
},
computed: {
......@@ -61,43 +69,53 @@ export default {
toggleShowLess() {
this.showLess = !this.showLess;
},
reRequestReview(userId) {
this.loading = true;
this.$emit('request-review', { userId, callback: this.requestReviewComplete });
},
requestReviewComplete(success) {
if (success) {
this.requestedReviewSuccess = true;
setTimeout(() => {
this.requestedReviewSuccess = false;
}, 1500);
}
this.loading = false;
},
},
};
</script>
<template>
<reviewer-avatar-link
v-if="hasOneUser"
#default="{ user }"
tooltip-placement="left"
:tooltip-has-name="false"
:user="firstUser"
:root-path="rootPath"
:issuable-type="issuableType"
>
<div class="gl-ml-3 gl-line-height-normal">
<div class="author">{{ user.name }}</div>
<div class="username">{{ username }}</div>
</div>
</reviewer-avatar-link>
<div v-else>
<div class="user-list">
<div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
</div>
</div>
<div v-if="renderShowMoreSection" class="user-list-more">
<button
type="button"
class="btn-link"
data-qa-selector="more_reviewers_link"
@click="toggleShowLess"
>
<template v-if="showLess">
{{ hiddenReviewersLabel }}
</template>
<template v-else>{{ __('- show less') }}</template>
</button>
<div>
<div
v-for="(user, index) in users"
:key="user.id"
:class="{ 'gl-mb-3': index !== users.length - 1 }"
data-testid="reviewer"
>
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType">
<div class="gl-ml-3">@{{ user.username }}</div>
</reviewer-avatar-link>
<gl-icon
v-if="requestedReviewSuccess"
:size="24"
name="check"
class="float-right gl-text-green-500"
/>
<gl-button
v-else-if="user.can_update_merge_request && user.reviewed"
v-gl-tooltip.left
:title="__('Re-request review')"
:loading="loading"
class="float-right gl-text-gray-500!"
size="small"
icon="redo"
variant="link"
@click="reRequestReview(user.id)"
/>
</div>
</div>
</template>
......@@ -42,7 +42,7 @@ export default {
buttonClasses() {
return this.collapsed
? 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state'
: 'btn btn-default btn-todo issuable-header-btn float-right';
: 'gl-button btn btn-default btn-todo issuable-header-btn float-right';
},
buttonLabel() {
return this.isTodo ? MARK_TEXT : TODO_TEXT;
......
mutation mergeRequestRequestRereview($projectPath: ID!, $iid: String!, $userId: ID!) {
mergeRequestReviewerRereview(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
errors
}
}
import sidebarDetailsQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
import axios from '~/lib/utils/axios_utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
export const gqClient = createGqClient(
{},
......@@ -70,4 +72,15 @@ export default class SidebarService {
move_to_project_id: moveToProjectId,
});
}
requestReview(userId) {
return gqClient.mutate({
mutation: reviewerRereviewMutation,
variables: {
userId: convertToGraphQLId('User', `${userId}`), // eslint-disable-line @gitlab/require-i18n-strings
projectPath: this.fullPath,
iid: this.iid.toString(),
},
});
}
}
import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import { visitUrl } from '../lib/utils/url_utility';
import { deprecatedCreateFlash as Flash } from '../flash';
......@@ -51,6 +52,17 @@ export default class SidebarMediator {
return this.service.update(field, data);
}
requestReview({ userId, callback }) {
return this.service
.requestReview(userId)
.then(() => {
this.store.updateReviewer(userId);
toast(__('Requested review'));
callback(true);
})
.catch(() => callback(false));
}
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}
......
......@@ -96,6 +96,14 @@ export default class SidebarStore {
}
}
updateReviewer(id) {
const reviewer = this.findReviewer({ id });
if (reviewer) {
reviewer.reviewed = false;
}
}
findAssignee(findAssignee) {
return this.assignees.find(({ id }) => id === findAssignee.id);
}
......
# frozen_string_literal: true
module CommentAndCloseFlag
extend ActiveSupport::Concern
included do
before_action do
push_frontend_feature_flag(:remove_comment_close_reopen, @group)
end
end
end
......@@ -9,6 +9,7 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuesCalendar
include SpammableActions
include RecordUserLastActivity
include CommentAndCloseFlag
ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk].freeze
SET_ISSUEABLES_INDEX_ONLY_ACTIONS = %i[index calendar service_desk].freeze
......
......@@ -11,6 +11,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include RecordUserLastActivity
include SourcegraphDecorator
include DiffHelper
include CommentAndCloseFlag
skip_before_action :merge_request, only: [:index, :bulk_update, :export_csv]
before_action :apply_diff_view_cookie!, only: [:show]
......@@ -22,7 +23,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
:coverage_reports,
:terraform_reports,
:accessibility_reports,
:codequality_reports
:codequality_reports,
:codequality_mr_diff_reports
]
before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues]
......@@ -66,7 +68,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
:toggle_award_emoji, :toggle_subscription, :update
]
feature_category :code_testing, [:test_reports, :coverage_reports]
feature_category :code_testing, [:test_reports, :coverage_reports, :codequality_mr_diff_reports]
feature_category :accessibility_testing, [:accessibility_reports]
feature_category :infrastructure_as_code, [:terraform_reports]
......@@ -195,6 +197,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
end
def codequality_mr_diff_reports
reports_response(@merge_request.find_codequality_mr_diff_reports)
end
def codequality_reports
reports_response(@merge_request.compare_codequality_reports)
end
......
......@@ -49,7 +49,7 @@ module Ci
end
def filter_by_scope(builds)
return filter_by_statuses!(params[:scope], builds) if params[:scope].is_a?(Array)
return filter_by_statuses!(builds) if params[:scope].is_a?(Array)
case params[:scope]
when 'pending'
......@@ -63,7 +63,7 @@ module Ci
end
end
def filter_by_statuses!(statuses, builds)
def filter_by_statuses!(builds)
unknown_statuses = params[:scope] - ::CommitStatus::AVAILABLE_STATUSES
raise ArgumentError, 'Scope contains invalid value(s)' unless unknown_statuses.empty?
......
......@@ -8,7 +8,6 @@ module Ci
"project_path" => @project.full_path,
"artifact_help_url" => help_page_path('user/gitlab_com/index.html', anchor: 'gitlab-cicd'),
"deployment_help_url" => help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting'),
"runner_help_url" => help_page_path('ci/runners/README.html', anchor: 'set-maximum-job-timeout-for-a-runner'),
"runner_settings_url" => project_runners_path(@build.project, anchor: 'js-runners-settings'),
"variables_settings_url" => project_variables_path(@build.project, anchor: 'js-cicd-variables-settings'),
"page_path" => project_job_path(@project, @build),
......
......@@ -203,6 +203,17 @@ module DiffHelper
set_secure_cookie(:diff_view, params.delete(:view), type: CookiesHelper::COOKIE_TYPE_PERMANENT) if params[:view].present?
end
def collapsed_diff_url(diff_file)
url_for(
safe_params.merge(
action: :diff_for_path,
old_path: diff_file.old_path,
new_path: diff_file.new_path,
file_identifier: diff_file.file_identifier
)
)
end
private
def diff_btn(title, name, selected)
......
......@@ -17,7 +17,7 @@ module OperationsHelper
'prometheus_authorization_key' => @project.alerting_setting&.token,
'prometheus_api_url' => prometheus_service.api_url,
'prometheus_url' => notify_project_prometheus_alerts_url(@project, format: :json),
'alerts_setup_url' => help_page_path('operations/incident_management/alert_integrations.md', anchor: 'generic-http-endpoint'),
'alerts_setup_url' => help_page_path('operations/incident_management/integrations.md', anchor: 'configuration'),
'alerts_usage_url' => project_alert_management_index_path(@project),
'disabled' => disabled.to_s,
'project_path' => @project.full_path,
......
# frozen_string_literal: true
# Backing store for GitLab session data.
#
# The raw session information is stored by the Rails session store
# (config/initializers/session_store.rb). These entries are accessible by the
# rack_key_name class method and consistute the base of the session data
# entries. All other entries in the session store can be traced back to these
# entries.
#
# After a user logs in (config/initializers/warden.rb) a further entry is made
# in Redis. This entry holds a record of the user's logged in session. These
# are accessible with the key_name(user_id, session_id) class method. These
# entries will expire. Lookups to these entries are lazilly cleaned on future
# user access.
#
# There is a reference to all sessions that belong to a specific user. A
# user may login through multiple browsers/devices and thus record multiple
# login sessions. These are accessible through the lookup_key_name(user_id)
# class method.
#
class ActiveSession
include ActiveModel::Model
......@@ -143,6 +162,10 @@ class ActiveSession
list(user).reject(&:is_impersonated)
end
def self.rack_key_name(session_id)
"#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}"
end
def self.key_name(user_id, session_id = '*')
"#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
end
......@@ -197,7 +220,7 @@ class ActiveSession
end
def self.rack_session_keys(rack_session_ids)
rack_session_ids.map { |session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
rack_session_ids.map { |session_id| rack_key_name(session_id)}
end
def self.raw_active_session_entries(redis, session_ids, user_id)
......
......@@ -44,6 +44,8 @@ class ApplicationSetting < ApplicationRecord
serialize :domain_denylist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
serialize :asset_proxy_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
# See https://gitlab.com/gitlab-org/gitlab/-/issues/300916
serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
......
......@@ -76,6 +76,22 @@ module Ci
end
end
##
# Sometime we need to ensure that the first read goes to a primary
# database, what is especially important in EE. This method does not
# change the behavior in CE.
#
def with_read_consistency(build, &block)
return yield unless consistent_reads_enabled?(build)
::Gitlab::Database::Consistency
.with_read_consistency(&block)
end
def consistent_reads_enabled?(build)
Feature.enabled?(:gitlab_ci_trace_read_consistency, build.project, type: :development, default_enabled: false)
end
##
# Sometimes we do not want to read raw data. This method makes it easier
# to find attributes that are just metadata excluding raw data.
......@@ -154,8 +170,8 @@ module Ci
in_lock(lock_key, **lock_params) do # exclusive Redis lock is acquired first
raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save?
self.reset.then do |chunk| # we ensure having latest lock_version
chunk.unsafe_persist_data! # we migrate the data and update data store
self.class.with_read_consistency(build) do
self.reset.then { |chunk| chunk.unsafe_persist_data! }
end
end
rescue FailedToObtainLockError
......
......@@ -1003,8 +1003,8 @@ module Ci
has_reports?(Ci::JobArtifact.coverage_reports)
end
def has_codequality_reports?
pipeline_artifacts&.has_report?(:code_quality)
def has_codequality_mr_diff_report?
pipeline_artifacts&.has_report?(:code_quality_mr_diff)
end
def can_generate_codequality_reports?
......
......@@ -15,7 +15,7 @@ module Ci
DEFAULT_FILE_NAMES = {
code_coverage: 'code_coverage.json',
code_quality: 'code_quality.json'
code_quality_mr_diff: 'code_quality_mr_diff.json'
}.freeze
belongs_to :project, class_name: "Project", inverse_of: :pipeline_artifacts
......@@ -32,7 +32,7 @@ module Ci
enum file_type: {
code_coverage: 1,
code_quality: 2
code_quality_mr_diff: 2
}
class << self
......
......@@ -1484,6 +1484,24 @@ class MergeRequest < ApplicationRecord
compare_reports(Ci::GenerateCoverageReportsService)
end
def has_codequality_mr_diff_report?
return false unless ::Gitlab::Ci::Features.display_quality_on_mr_diff?(project)
actual_head_pipeline&.has_codequality_mr_diff_report?
end
# TODO: this method and compare_test_reports use the same
# result type, which is handled by the controller's #reports_response.
# we should minimize mistakes by isolating the common parts.
# issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
def find_codequality_mr_diff_reports
unless has_codequality_mr_diff_report?
return { status: :error, status_reason: 'This merge request does not have codequality mr diff reports' }
end
compare_reports(Ci::GenerateCodequalityMrDiffReportService)
end
def has_codequality_reports?
return false unless ::Gitlab::Ci::Features.display_quality_on_mr_diff?(project)
......
......@@ -30,8 +30,8 @@ class ConfluenceService < Service
s_('ConfluenceService|Connect a Confluence Cloud Workspace to GitLab')
end
def detailed_description
return unless project.wiki_enabled?
def help
return unless project&.wiki_enabled?
if activated?
wiki_url = project.wiki.web_url
......
......@@ -2,7 +2,7 @@
module Ci
module PipelineArtifacts
class CodeCoveragePresenter < ProcessablePresenter
class CodeCoveragePresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize
def for_files(filenames)
......
# frozen_string_literal: true
module Ci
module PipelineArtifacts
class CodeQualityMrDiffPresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize
def for_files(filenames)
quality_files = raw_report["files"].select { |key| filenames.include?(key) }
{ files: quality_files }
end
private
def raw_report
strong_memoize(:raw_report) do
self.each_blob do |blob|
Gitlab::Json.parse(blob).with_indifferent_access
end
end
end
end
end
end
# frozen_string_literal: true
module Ci
# TODO: a couple of points with this approach:
# + reuses existing architecture and reactive caching
# - it's not a report comparison and some comparing features must be turned off.
# see CompareReportsBaseService for more notes.
# issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
class GenerateCodequalityMrDiffReportService < CompareReportsBaseService
def execute(base_pipeline, head_pipeline)
merge_request = MergeRequest.find_by_id(params[:id])
{
status: :parsed,
key: key(base_pipeline, head_pipeline),
data: head_pipeline.pipeline_artifacts.find_by_file_type(:code_quality_mr_diff).present.for_files(merge_request.new_paths)
}
rescue => e
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
{
status: :error,
key: key(base_pipeline, head_pipeline),
status_reason: _('An error occurred while fetching codequality mr diff reports.')
}
end
def latest?(base_pipeline, head_pipeline, data)
data&.fetch(:key, nil) == key(base_pipeline, head_pipeline)
end
end
end
# frozen_string_literal: true
module Ci
module PipelineArtifacts
class CreateQualityReportService
class CreateCodeQualityMrDiffReportService
def execute(pipeline)
return unless pipeline.can_generate_codequality_reports?
return if pipeline.has_codequality_reports?
return if pipeline.has_codequality_mr_diff_report?
file = build_carrierwave_file(pipeline)
pipeline.pipeline_artifacts.create!(
project_id: pipeline.project_id,
file_type: :code_quality,
file_type: :code_quality_mr_diff,
file_format: :raw,
size: file["tempfile"].size,
file: file,
......@@ -23,7 +23,7 @@ module Ci
def build_carrierwave_file(pipeline)
CarrierWaveStringFile.new_file(
file_content: build_quality_mr_diff_report(pipeline),
filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_quality),
filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_quality_mr_diff),
content_type: 'application/json'
)
end
......
# frozen_string_literal: true
module UpdateRepositoryStorageMethods
include Gitlab::Utils::StrongMemoize
Error = Class.new(StandardError)
SameFilesystemError = Class.new(Error)
attr_reader :repository_storage_move
delegate :container, :source_storage_name, :destination_storage_name, to: :repository_storage_move
......@@ -18,9 +19,7 @@ module UpdateRepositoryStorageMethods
repository_storage_move.start!
end
raise SameFilesystemError if same_filesystem?(source_storage_name, destination_storage_name)
mirror_repositories
mirror_repositories unless same_filesystem?
repository_storage_move.transaction do
repository_storage_move.finish_replication!
......@@ -28,8 +27,10 @@ module UpdateRepositoryStorageMethods
track_repository(destination_storage_name)
end
remove_old_paths
enqueue_housekeeping
unless same_filesystem?
remove_old_paths
enqueue_housekeeping
end
repository_storage_move.finish_cleanup!
......@@ -80,8 +81,10 @@ module UpdateRepositoryStorageMethods
end
end
def same_filesystem?(old_storage, new_storage)
Gitlab::GitalyClient.filesystem_id(old_storage) == Gitlab::GitalyClient.filesystem_id(new_storage)
def same_filesystem?
strong_memoize(:same_filesystem) do
Gitlab::GitalyClient.filesystem_id(source_storage_name) == Gitlab::GitalyClient.filesystem_id(destination_storage_name)
end
end
def remove_old_paths
......
......@@ -126,3 +126,5 @@ module Groups
end
end
end
Groups::ImportExport::ExportService.prepend_if_ee('EE::Groups::ImportExport::ExportService')
......@@ -123,3 +123,5 @@ module Groups
end
end
end
Groups::ImportExport::ImportService.prepend_if_ee('EE::Groups::ImportExport::ImportService')
......@@ -24,9 +24,7 @@ module Pages
@queue.close
@logger.info("Waiting for threads to finish...")
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
threads.each(&:join)
end
threads.each(&:join)
{ migrated: @migrated, errored: @errored }
end
......@@ -34,8 +32,8 @@ module Pages
def start_migration_threads
Array.new(@migration_threads) do
Thread.new do
Rails.application.executor.wrap do
while batch = @queue.pop
while batch = @queue.pop
Rails.application.executor.wrap do
process_batch(batch)
end
end
......@@ -51,6 +49,11 @@ module Pages
end
@logger.info("#{@migrated} projects are migrated successfully, #{@errored} projects failed to be migrated")
rescue => e
# This method should never raise exception otherwise all threads might be killed
# and this will result in queue starving (and deadlock)
Gitlab::ErrorTracking.track_exception(e)
@logger.error("failed processing a batch: #{e.message}")
end
def migrate_project(project)
......
......@@ -3,7 +3,7 @@
.svg-content.svg-250
= image_tag 'illustrations/starred_empty.svg'
.text-content
%h4.text-center
%h4.gl-text-center
= s_("StarredProjectsEmptyState|You don't have starred projects yet.")
%p.text-secondary
%p.gl-text-gray-500
= s_("StarredProjectsEmptyState|Visit a project page and press on a star icon. Then, you can find the project on this page.")
- diff_file = viewer.diff_file
- url = url_for(safe_params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
.nothing-here-block.diff-collapsed{ data: { diff_for_path: url } }
.nothing-here-block.diff-collapsed{ data: { diff_for_path: collapsed_diff_url(viewer.diff_file) } }
= _("This diff is collapsed.")
%button.click-to-expand.btn.btn-link= _("Click to expand it.")
......@@ -3,6 +3,6 @@
.sub-header-block.bg-gray-light.gl-p-5
.tree-ref-holder.inline.vertical-align-middle
= render 'shared/ref_switcher', destination: 'graphs'
= link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn gl-button'
= link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn gl-button btn-default'
.js-contributors-graph{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json),'data-project-branch': current_ref }
- if lookup_context.template_exists?('top', "projects/services/#{@service.to_param}", true)
= render "projects/services/#{@service.to_param}/top"
.row.gl-mt-3.gl-mb-3
.col-lg-4
%h3.page-title.gl-mt-0
= @service.title
- if @service.operating?
= sprite_icon('check', css_class: 'gl-text-green-500')
%h3.page-title
= @service.title
- if @service.operating?
= sprite_icon('check', css_class: 'gl-text-green-500')
- if @service.respond_to?(:detailed_description)
%p= @service.detailed_description
.col-lg-8
= form_for(@service, as: :service, url: scoped_integration_path(@service), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= render 'shared/service_settings', form: form, integration: @service
%input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer }
= form_for(@service, as: :service, url: scoped_integration_path(@service), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= render 'shared/service_settings', form: form, integration: @service
%input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer }
- if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true)
%hr
......
......@@ -15,5 +15,5 @@
= render 'shared/notes/hints'
.error-alert
.gl-mt-3
= f.submit 'Save changes', class: 'btn btn-success'
= link_to "Cancel", project_tag_path(@project, @tag.name), class: "btn btn-default btn-cancel"
= f.submit 'Save changes', class: 'btn gl-button btn-success'
= link_to "Cancel", project_tag_path(@project, @tag.name), class: "btn gl-button btn-default btn-cancel"
......@@ -42,13 +42,13 @@
- if @tag.has_signature?
= render partial: 'projects/commit/signature', object: @tag.signature
- if can?(current_user, :admin_tag, @project)
= link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn btn-icon btn-edit gl-button controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do
= link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn btn-icon btn-edit gl-button btn-default controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do
= sprite_icon("pencil", css_class: 'gl-icon')
= link_to project_tree_path(@project, @tag.name), class: 'btn btn-icon gl-button controls-item has-tooltip', title: s_('TagsPage|Browse files') do
= link_to project_tree_path(@project, @tag.name), class: 'btn btn-icon gl-button btn-default controls-item has-tooltip', title: s_('TagsPage|Browse files') do
= sprite_icon('folder-open', css_class: 'gl-icon')
= link_to project_commits_path(@project, @tag.name), class: 'btn btn-icon gl-button controls-item has-tooltip', title: s_('TagsPage|Browse commits') do
= link_to project_commits_path(@project, @tag.name), class: 'btn btn-icon gl-button btn-default controls-item has-tooltip', title: s_('TagsPage|Browse commits') do
= sprite_icon('history', css_class: 'gl-icon')
.btn-container.controls-item
.controls-item
= render 'projects/buttons/download', project: @project, ref: @tag.name
- if can?(current_user, :admin_tag, @project)
.btn-container.controls-item-full
......
......@@ -43,10 +43,10 @@
- if current_user
%li.inline.label-subscription
- if label.can_subscribe_to_label_in_different_levels?
%button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
%button.js-unsubscribe-button.gl-button.label-subscribe-button.btn.btn-default.gl-ml-3{ class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
%span= _('Unsubscribe')
.dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
%button.gl-button.label-subscribe-button.btn.btn-default{ data: { toggle: 'dropdown' } }
%button.gl-button.label-subscribe-button.btn.btn-default.gl-ml-3{ data: { toggle: 'dropdown' } }
%span
= _('Subscribe')
= sprite_icon('chevron-down')
......@@ -59,7 +59,7 @@
%button.js-subscribe-button.js-group-level.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } }
%span= _('Subscribe at group level')
- else
%button.gl-button.js-subscribe-button.label-subscribe-button.btn.btn-default{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
%button.gl-button.js-subscribe-button.label-subscribe-button.btn.btn-default.gl-ml-3{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
%span= label_subscription_toggle_button_text(label, @project)
= render 'shared/delete_label_modal', label: label
......@@ -4,7 +4,7 @@
- if @default_integration
.js-vue-default-integration-settings{ data: integration_form_data(@default_integration, group: @group) }
.js-vue-integration-settings{ data: integration_form_data(integration, group: @group) }
.js-integration-help-html
.js-integration-help-html.gl-display-none
-# All content below will be repositioned in Vue
- if lookup_context.template_exists?('help', "projects/services/#{integration.to_param}", true)
= render "projects/services/#{integration.to_param}/help", subject: integration
......
- integration = local_assigns.fetch(:integration)
.row.gl-mt-3
.col-lg-4
%h3.page-title.gl-mt-0
= integration.title
%h3.page-title
= integration.title
.col-lg-8
= form_for integration, as: :service, url: scoped_integration_path(integration), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => scoped_test_integration_path(integration) } } do |form|
= render 'shared/service_settings', form: form, integration: integration
= form_for integration, as: :service, url: scoped_integration_path(integration), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => scoped_test_integration_path(integration) } } do |form|
= render 'shared/service_settings', form: form, integration: integration
......@@ -6,7 +6,7 @@
- button_icon = has_todo ? todo_button_data[:mark_icon] : todo_button_data[:todo_icon]
%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn float-right'),
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'gl-button btn btn-default issuable-header-btn float-right'),
title: button_title,
'aria-label' => button_title,
data: todo_button_data }
......
......@@ -12,7 +12,7 @@ module Ci
def perform(pipeline_id)
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
Ci::PipelineArtifacts::CreateQualityReportService.new.execute(pipeline)
Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService.new.execute(pipeline)
end
end
end
......
---
title: 'Editor Lite: support for Diff Instance'
merge_request: 51470
author:
type: added
---
title: Add btn-default class for history button in the contributors page
merge_request: 52861
author: Yogi (@yo)
type: other
---
title: Lower allocations in _collapsed partial
merge_request: 53233
author:
type: performance
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment