Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-06-30 15:08:48 +00:00
parent 1e254d9f5a
commit 340f15b402
128 changed files with 2365 additions and 980 deletions

View File

@ -16,25 +16,24 @@ review-cleanup:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb
- gcp_cleanup
# Temporarily disabling review apps
#review-build-cng:
# extends:
# - .default-retry
# - .review:rules:review-build-cng
# image: ruby:2.6-alpine
# stage: review-prepare
# before_script:
# - source scripts/utils.sh
# - install_api_client_dependencies_with_apk
# - install_gitlab_gem
# needs:
# - job: compile-production-assets
# artifacts: false
# script:
# - BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
# # When the job is manual, review-deploy is also manual and we don't want people
# # to have to manually start the jobs in sequence, so we do it for them.
# - '[ -z $CI_JOB_MANUAL ] || play_job "review-deploy"'
review-build-cng:
extends:
- .default-retry
- .review:rules:review-build-cng
image: ruby:2.6-alpine
stage: review-prepare
before_script:
- source scripts/utils.sh
- install_api_client_dependencies_with_apk
- install_gitlab_gem
needs:
- job: compile-production-assets
artifacts: false
script:
- BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
# When the job is manual, review-deploy is also manual and we don't want people
# to have to manually start the jobs in sequence, so we do it for them.
- '[ -z $CI_JOB_MANUAL ] || play_job "review-deploy"'
.review-workflow-base:
extends:
@ -42,6 +41,7 @@ review-cleanup:
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-helm3-kubectl1.14
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
REVIEW_APPS_DOMAIN: "temp.gitlab-review.app" # FIXME: using temporary domain
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "master"
environment:
@ -50,37 +50,37 @@ review-cleanup:
on_stop: review-stop
auto_stop_in: 48 hours
# Temporarily disabling review apps
#review-deploy:
# extends:
# - .review-workflow-base
# - .review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise
# stage: review
# dependencies: []
# resource_group: "review/${CI_COMMIT_REF_NAME}"
# before_script:
# - export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
# - export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
# - export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
# - echo "${CI_ENVIRONMENT_URL}" > environment_url.txt
# - source ./scripts/utils.sh
# - install_api_client_dependencies_with_apk
# - source scripts/review_apps/review-apps.sh
# script:
# - check_kube_domain
# - ensure_namespace
# - install_external_dns
# - download_chart
# - date
# - deploy || (display_deployment_debug && exit 1)
# # When the job is manual, review-qa-smoke is also manual and we don't want people
# # to have to manually start the jobs in sequence, so we do it for them.
# - '[ -z $CI_JOB_MANUAL ] || play_job "review-qa-smoke"'
# - '[ -z $CI_JOB_MANUAL ] || play_job "review-performance"'
# artifacts:
# paths: [environment_url.txt]
# expire_in: 2 days
# when: always
review-deploy:
extends:
- .review-workflow-base
- .review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise
stage: review
dependencies: []
resource_group: "review/${CI_COMMIT_REF_NAME}"
before_script:
- export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
- export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
- export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
- echo "${CI_ENVIRONMENT_URL}" > environment_url.txt
- source ./scripts/utils.sh
- install_api_client_dependencies_with_apk
- source scripts/review_apps/review-apps.sh
script:
- check_kube_domain
- ensure_namespace
- install_external_dns
- download_chart
- date
- deploy || (display_deployment_debug && exit 1)
- disable_sign_ups
# When the job is manual, review-qa-smoke is also manual and we don't want people
# to have to manually start the jobs in sequence, so we do it for them.
- '[ -z $CI_JOB_MANUAL ] || play_job "review-qa-smoke"'
- '[ -z $CI_JOB_MANUAL ] || play_job "review-performance"'
artifacts:
paths: [environment_url.txt]
expire_in: 2 days
when: always
.review-stop-base:
extends: .review-workflow-base
@ -113,110 +113,110 @@ review-stop:
script:
- delete_release
# Temporarily disabling review apps
#.review-qa-base:
# extends:
# - .default-retry
# - .use-docker-in-docker
# image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6
# stage: qa
# # This is needed so that manual jobs with needs don't block the pipeline.
# # See https://gitlab.com/gitlab-org/gitlab/-/issues/199979.
# dependencies: ["review-deploy"]
# variables:
# QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa"
# QA_CAN_TEST_GIT_PROTOCOL_V2: "false"
# QA_DEBUG: "true"
# GITLAB_USERNAME: "root"
# GITLAB_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
# GITLAB_ADMIN_USERNAME: "root"
# GITLAB_ADMIN_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
# GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
# EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
# before_script:
# - export QA_IMAGE="${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}"
# - export CI_ENVIRONMENT_URL="$(cat environment_url.txt)"
# - echo "${CI_ENVIRONMENT_URL}"
# - echo "${QA_IMAGE}"
# - source scripts/utils.sh
# - install_api_client_dependencies_with_apk
# - gem install gitlab-qa --no-document ${GITLAB_QA_VERSION:+ --version ${GITLAB_QA_VERSION}}
# artifacts:
# paths:
# - ./qa/gitlab-qa-run-*
# expire_in: 7 days
# when: always
#
#review-qa-smoke:
# extends:
# - .review-qa-base
# - .review:rules:review-qa-smoke
# script:
# - gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
#
#review-qa-all:
# extends:
# - .review-qa-base
# - .review:rules:mr-only-manual
# parallel: 5
# script:
# - export KNAPSACK_REPORT_PATH=knapsack/master_report.json
# - export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb
# - gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" -- --format RspecJunitFormatter --out tmp/rspec-${CI_JOB_ID}.xml --format html --out tmp/rspec.htm --color --format documentation
#
#review-performance:
# extends:
# - .default-retry
# - .review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise
# image:
# name: sitespeedio/sitespeed.io:6.3.1
# entrypoint: [""]
# stage: qa
# # This is needed so that manual jobs with needs don't block the pipeline.
# # See https://gitlab.com/gitlab-org/gitlab/-/issues/199979.
# dependencies: ["review-deploy"]
# before_script:
# - export CI_ENVIRONMENT_URL="$(cat environment_url.txt)"
# - echo "${CI_ENVIRONMENT_URL}"
# - mkdir -p gitlab-exporter
# - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
# - mkdir -p sitespeed-results
# script:
# - /start.sh --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "${CI_ENVIRONMENT_URL}"
# after_script:
# - mv sitespeed-results/data/performance.json performance.json
# artifacts:
# paths:
# - sitespeed-results/
# reports:
# performance: performance.json
# expire_in: 31d
#
#parallel-spec-reports:
# extends:
# - .review:rules:mr-only-manual
# image: ruby:2.6-alpine
# stage: post-qa
# dependencies: ["review-qa-all"]
# variables:
# NEW_PARALLEL_SPECS_REPORT: qa/report-new.html
# BASE_ARTIFACT_URL: "${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/file/qa/"
# script:
# - apk add --update build-base libxml2-dev libxslt-dev && rm -rf /var/cache/apk/*
# - gem install nokogiri --no-document
# - cd qa/gitlab-qa-run-*/gitlab-*
# - ARTIFACT_DIRS=$(pwd |rev| awk -F / '{print $1,$2}' | rev | sed s_\ _/_)
# - cd -
# - '[[ -f $NEW_PARALLEL_SPECS_REPORT ]] || echo "{}" > ${NEW_PARALLEL_SPECS_REPORT}'
# - scripts/merge-html-reports ${NEW_PARALLEL_SPECS_REPORT} ${BASE_ARTIFACT_URL}${ARTIFACT_DIRS} qa/gitlab-qa-run-*/**/rspec.htm
# artifacts:
# when: always
# paths:
# - qa/report-new.html
# - qa/gitlab-qa-run-*
# reports:
# junit: qa/gitlab-qa-run-*/**/rspec-*.xml
# expire_in: 31d
.review-qa-base:
extends:
- .default-retry
- .use-docker-in-docker
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6
stage: qa
# This is needed so that manual jobs with needs don't block the pipeline.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/199979.
dependencies: ["review-deploy"]
variables:
QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa"
QA_CAN_TEST_GIT_PROTOCOL_V2: "false"
QA_DEBUG: "true"
GITLAB_USERNAME: "root"
GITLAB_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
GITLAB_ADMIN_USERNAME: "root"
GITLAB_ADMIN_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
SIGNUP_DISABLED: "true"
before_script:
- export QA_IMAGE="${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}"
- export CI_ENVIRONMENT_URL="$(cat environment_url.txt)"
- echo "${CI_ENVIRONMENT_URL}"
- echo "${QA_IMAGE}"
- source scripts/utils.sh
- install_api_client_dependencies_with_apk
- gem install gitlab-qa --no-document ${GITLAB_QA_VERSION:+ --version ${GITLAB_QA_VERSION}}
artifacts:
paths:
- ./qa/gitlab-qa-run-*
expire_in: 7 days
when: always
review-qa-smoke:
extends:
- .review-qa-base
- .review:rules:review-qa-smoke
script:
- gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
review-qa-all:
extends:
- .review-qa-base
- .review:rules:mr-only-manual
parallel: 5
script:
- export KNAPSACK_REPORT_PATH=knapsack/master_report.json
- export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb
- gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" -- --format RspecJunitFormatter --out tmp/rspec-${CI_JOB_ID}.xml --format html --out tmp/rspec.htm --color --format documentation
review-performance:
extends:
- .default-retry
- .review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise
image:
name: sitespeedio/sitespeed.io:6.3.1
entrypoint: [""]
stage: qa
# This is needed so that manual jobs with needs don't block the pipeline.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/199979.
dependencies: ["review-deploy"]
before_script:
- export CI_ENVIRONMENT_URL="$(cat environment_url.txt)"
- echo "${CI_ENVIRONMENT_URL}"
- mkdir -p gitlab-exporter
- wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- mkdir -p sitespeed-results
script:
- /start.sh --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "${CI_ENVIRONMENT_URL}"
after_script:
- mv sitespeed-results/data/performance.json performance.json
artifacts:
paths:
- sitespeed-results/
reports:
performance: performance.json
expire_in: 31d
parallel-spec-reports:
extends:
- .review:rules:mr-only-manual
image: ruby:2.6-alpine
stage: post-qa
dependencies: ["review-qa-all"]
variables:
NEW_PARALLEL_SPECS_REPORT: qa/report-new.html
BASE_ARTIFACT_URL: "${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/file/qa/"
script:
- apk add --update build-base libxml2-dev libxslt-dev && rm -rf /var/cache/apk/*
- gem install nokogiri --no-document
- cd qa/gitlab-qa-run-*/gitlab-*
- ARTIFACT_DIRS=$(pwd |rev| awk -F / '{print $1,$2}' | rev | sed s_\ _/_)
- cd -
- '[[ -f $NEW_PARALLEL_SPECS_REPORT ]] || echo "{}" > ${NEW_PARALLEL_SPECS_REPORT}'
- scripts/merge-html-reports ${NEW_PARALLEL_SPECS_REPORT} ${BASE_ARTIFACT_URL}${ARTIFACT_DIRS} qa/gitlab-qa-run-*/**/rspec.htm
artifacts:
when: always
paths:
- qa/report-new.html
- qa/gitlab-qa-run-*
reports:
junit: qa/gitlab-qa-run-*/**/rspec-*.xml
expire_in: 31d
danger-review:
extends:

View File

@ -66,7 +66,7 @@ gem 'u2f', '~> 0.2.1'
gem 'validates_hostname', '~> 1.0.10'
gem 'rubyzip', '~> 2.0.0', require: 'zip'
# GitLab Pages letsencrypt support
gem 'acme-client', '~> 2.0.5'
gem 'acme-client', '~> 2.0', '>= 2.0.6'
# Browser detection
gem 'browser', '~> 2.5'

View File

@ -4,8 +4,8 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2)
acme-client (2.0.5)
faraday (~> 0.9, >= 0.9.1)
acme-client (2.0.6)
faraday (>= 0.17, < 2.0.0)
actioncable (6.0.3.1)
actionpack (= 6.0.3.1)
nio4r (~> 2.0)
@ -1169,7 +1169,7 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
acme-client (~> 2.0.5)
acme-client (~> 2.0, >= 2.0.6)
activerecord-explain-analyze (~> 0.1)
acts-as-taggable-on (~> 6.0)
addressable (~> 2.7)

View File

@ -12,13 +12,15 @@ import {
GlTable,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import query from '../graphql/queries/details.query.graphql';
import alertQuery from '../graphql/queries/details.query.graphql';
import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import initUserPopovers from '~/user_popovers';
import { ALERTS_SEVERITY_LABELS, trackAlertsDetailsViewsOptions } from '../constants';
import createIssueQuery from '../graphql/mutations/create_issue_from_alert.graphql';
import createIssueMutation from '../graphql/mutations/create_issue_from_alert.graphql';
import toggleSidebarStatusMutation from '../graphql/mutations/toggle_sidebar_status.mutation.graphql';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import { toggleContainerClasses } from '~/lib/utils/dom_utils';
@ -52,28 +54,27 @@ export default {
AlertSidebar,
SystemNote,
},
props: {
inject: {
projectPath: {
default: '',
},
alertId: {
type: String,
required: true,
default: '',
},
projectId: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
default: '',
},
projectIssuesPath: {
type: String,
required: true,
default: '',
},
},
apollo: {
alert: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
query,
query: alertQuery,
variables() {
return {
fullPath: this.projectPath,
@ -88,15 +89,18 @@ export default {
Sentry.captureException(error);
},
},
sidebarStatus: {
query: sidebarStatusQuery,
},
},
data() {
return {
alert: null,
errored: false,
sidebarStatus: false,
isErrorDismissed: false,
createIssueError: '',
issueCreationInProgress: false,
sidebarCollapsed: false,
sidebarErrorMessage: '',
};
},
@ -132,10 +136,10 @@ export default {
this.sidebarErrorMessage = '';
},
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed;
this.$apollo.mutate({ mutation: toggleSidebarStatusMutation });
toggleContainerClasses(containerEl, {
'right-sidebar-collapsed': this.sidebarCollapsed,
'right-sidebar-expanded': !this.sidebarCollapsed,
'right-sidebar-collapsed': !this.sidebarStatus,
'right-sidebar-expanded': this.sidebarStatus,
});
},
handleAlertSidebarError(errorMessage) {
@ -147,7 +151,7 @@ export default {
this.$apollo
.mutate({
mutation: createIssueQuery,
mutation: createIssueMutation,
variables: {
iid: this.alert.iid,
projectPath: this.projectPath,
@ -197,7 +201,7 @@ export default {
<div
v-if="alert"
class="alert-management-details gl-relative"
:class="{ 'pr-sm-8': sidebarCollapsed }"
:class="{ 'pr-sm-8': sidebarStatus }"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-px-1 py-3 py-md-4 gl-border-b-1 gl-border-b-gray-200 gl-border-b-solid flex-column flex-sm-row"
@ -330,10 +334,7 @@ export default {
</gl-tab>
</gl-tabs>
<alert-sidebar
:project-path="projectPath"
:project-id="projectId"
:alert="alert"
:sidebar-collapsed="sidebarCollapsed"
@alert-refresh="alertRefresh"
@toggle-sidebar="toggleSidebar"
@alert-error="handleAlertSidebarError"

View File

@ -4,6 +4,8 @@ import SidebarTodo from './sidebar/sidebar_todo.vue';
import SidebarStatus from './sidebar/sidebar_status.vue';
import SidebarAssignees from './sidebar/sidebar_assignees.vue';
import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql';
export default {
components: {
SidebarAssignees,
@ -11,27 +13,34 @@ export default {
SidebarTodo,
SidebarStatus,
},
props: {
sidebarCollapsed: {
type: Boolean,
required: true,
inject: {
projectPath: {
default: '',
},
projectId: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
default: '',
},
},
props: {
alert: {
type: Object,
required: true,
},
},
apollo: {
sidebarStatus: {
query: sidebarStatusQuery,
},
},
data() {
return {
sidebarStatus: false,
};
},
computed: {
sidebarCollapsedClass() {
return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
return this.sidebarStatus ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
},
},
};
@ -41,10 +50,10 @@ export default {
<aside :class="sidebarCollapsedClass" class="right-sidebar alert-sidebar">
<div class="issuable-sidebar js-issuable-update">
<sidebar-header
:sidebar-collapsed="sidebarCollapsed"
:sidebar-collapsed="sidebarStatus"
@toggle-sidebar="$emit('toggle-sidebar')"
/>
<sidebar-todo v-if="sidebarCollapsed" :sidebar-collapsed="sidebarCollapsed" />
<sidebar-todo v-if="sidebarStatus" :sidebar-collapsed="sidebarStatus" />
<sidebar-status
:project-path="projectPath"
:alert="alert"
@ -55,7 +64,7 @@ export default {
:project-path="projectPath"
:project-id="projectId"
:alert="alert"
:sidebar-collapsed="sidebarCollapsed"
:sidebar-collapsed="sidebarStatus"
@alert-refresh="$emit('alert-refresh')"
@toggle-sidebar="$emit('toggle-sidebar')"
@alert-error="$emit('alert-error', $event)"

View File

@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import AlertDetails from './components/alert_details.vue';
import sidebarStatusQuery from './graphql/queries/sidebar_status.query.graphql';
Vue.use(VueApollo);
@ -10,39 +11,51 @@ export default selector => {
const domEl = document.querySelector(selector);
const { alertId, projectPath, projectIssuesPath, projectId } = domEl.dataset;
const resolvers = {
Mutation: {
toggleSidebarStatus: (_, __, { cache }) => {
const data = cache.readQuery({ query: sidebarStatusQuery });
data.sidebarStatus = !data.sidebarStatus;
cache.writeQuery({ query: sidebarStatusQuery, data });
},
},
};
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
cacheConfig: {
dataIdFromObject: object => {
// eslint-disable-next-line no-underscore-dangle
if (object.__typename === 'AlertManagementAlert') {
return object.iid;
}
return defaultDataIdFromObject(object);
},
defaultClient: createDefaultClient(resolvers, {
cacheConfig: {
dataIdFromObject: object => {
// eslint-disable-next-line no-underscore-dangle
if (object.__typename === 'AlertManagementAlert') {
return object.iid;
}
return defaultDataIdFromObject(object);
},
},
),
}),
});
apolloProvider.clients.defaultClient.cache.writeData({
data: {
sidebarStatus: false,
},
});
// eslint-disable-next-line no-new
new Vue({
el: selector,
provide: {
projectPath,
alertId,
projectIssuesPath,
projectId,
},
apolloProvider,
components: {
AlertDetails,
},
render(createElement) {
return createElement('alert-details', {
props: {
alertId,
projectPath,
projectId,
projectIssuesPath,
},
});
return createElement('alert-details', {});
},
});
};

View File

@ -1,4 +1,4 @@
mutation($projectPath: ID!, $assigneeUsernames: [String!]!, $iid: String!) {
mutation alertSetAssignees($projectPath: ID!, $assigneeUsernames: [String!]!, $iid: String!) {
alertSetAssignees(
input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $projectPath }
) {

View File

@ -1,4 +1,4 @@
mutation ($projectPath: ID!, $iid: String!) {
mutation createAlertIssue($projectPath: ID!, $iid: String!) {
createAlertIssue(input: { iid: $iid, projectPath: $projectPath }) {
errors
issue {

View File

@ -0,0 +1,3 @@
mutation toggleSidebarStatus {
toggleSidebarStatus @client
}

View File

@ -1,4 +1,4 @@
mutation ($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
mutation updateAlertStatus($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) {
errors
alert {

View File

@ -0,0 +1,3 @@
query sidebarStatus {
sidebarStatus @client
}

View File

@ -13,13 +13,14 @@ export default {
type: Array,
required: true,
},
},
inject: {
projectPath: {
type: String,
required: true,
default: '',
},
iid: {
type: String,
required: true,
from: 'issueIid',
defaut: '',
},
},
computed: {

View File

@ -60,7 +60,7 @@ export default {
},
mounted() {
if (this.isNoteLinked) {
this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
}
},
methods: {
@ -80,7 +80,7 @@ export default {
</script>
<template>
<timeline-entry-item :id="`note_${noteAnchorId}`" ref="anchor" class="design-note note-form">
<timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form">
<user-avatar-link
:link-href="author.webUrl"
:img-src="author.avatarUrl"

View File

@ -6,7 +6,6 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
import Pagination from './pagination.vue';
import DeleteButton from '../delete_button.vue';
import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql';
import appDataQuery from '../../graphql/queries/app_data.query.graphql';
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
export default {
@ -55,19 +54,17 @@ export default {
permissions: {
createDesign: false,
},
projectPath: '',
issueIid: null,
};
},
apollo: {
appData: {
query: appDataQuery,
manual: true,
result({ data: { projectPath, issueIid } }) {
this.projectPath = projectPath;
this.issueIid = issueIid;
},
inject: {
projectPath: {
default: '',
},
issueIid: {
default: '',
},
},
apollo: {
permissions: {
query: permissionsQuery,
variables() {
@ -102,6 +99,7 @@ export default {
query: $route.query,
}"
:aria-label="s__('DesignManagement|Go back to designs')"
data-testid="close-design"
class="mr-3 text-plain d-flex justify-content-center align-items-center"
>
<icon :size="18" name="close" />

View File

@ -1,4 +0,0 @@
query projectFullPath {
projectPath @client
issueIid @client
}

View File

@ -1,29 +1,15 @@
import $ from 'jquery';
import Vue from 'vue';
import createRouter from './router';
import App from './components/app.vue';
import apolloProvider from './graphql';
import getDesignListQuery from './graphql/queries/get_design_list.query.graphql';
import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants';
export default () => {
const el = document.querySelector('.js-design-management-new');
const badge = document.querySelector('.js-designs-count');
const { issueIid, projectPath, issuePath } = el.dataset;
const router = createRouter(issuePath);
$('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => {
if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) {
router.push({ name: DESIGNS_ROUTE_NAME });
} else if (id === 'discussion') {
router.push({ name: ROOT_ROUTE_NAME });
}
});
apolloProvider.clients.defaultClient.cache.writeData({
data: {
projectPath,
issueIid,
activeDiscussion: {
__typename: 'ActiveDiscussion',
id: null,
@ -32,25 +18,14 @@ export default () => {
},
});
apolloProvider.clients.defaultClient
.watchQuery({
query: getDesignListQuery,
variables: {
fullPath: projectPath,
iid: issueIid,
atVersion: null,
},
})
.subscribe(({ data }) => {
if (badge) {
badge.textContent = data.project.issue.designCollection.designs.edges.length;
}
});
return new Vue({
el,
router,
apolloProvider,
provide: {
projectPath,
issueIid,
},
render(createElement) {
return createElement(App);
},

View File

@ -1,17 +1,8 @@
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
import appDataQuery from '../graphql/queries/app_data.query.graphql';
import { findVersionId } from '../utils/design_management_utils';
export default {
apollo: {
appData: {
query: appDataQuery,
manual: true,
result({ data: { projectPath, issueIid } }) {
this.projectPath = projectPath;
this.issueIid = issueIid;
},
},
allVersions: {
query: getDesignListQuery,
variables() {
@ -24,6 +15,14 @@ export default {
update: data => data.project.issue.designCollection.versions.edges,
},
},
inject: {
projectPath: {
default: '',
},
issueIid: {
default: '',
},
},
computed: {
hasValidVersion() {
return (
@ -55,8 +54,6 @@ export default {
data() {
return {
allVersions: [],
projectPath: '',
issueIid: null,
};
},
};

View File

@ -12,7 +12,6 @@ import DesignPresentation from '../../components/design_presentation.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignSidebar from '../../components/design_sidebar.vue';
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
import appDataQuery from '../../graphql/queries/app_data.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql';
import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql';
import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
@ -62,22 +61,12 @@ export default {
design: {},
comment: '',
annotationCoordinates: null,
projectPath: '',
errorMessage: '',
issueIid: '',
scale: 1,
resolvedDiscussionsExpanded: false,
};
},
apollo: {
appData: {
query: appDataQuery,
manual: true,
result({ data: { projectPath, issueIid } }) {
this.projectPath = projectPath;
this.issueIid = issueIid;
},
},
design: {
query: getDesignQuery,
// We want to see cached design version if we have one, and fetch newer version on the background to update discussions

View File

@ -259,7 +259,7 @@ export default {
</script>
<template>
<div>
<div data-testid="designs-root">
<header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex">
<div class="d-flex justify-content-between align-items-center w-100">
<design-version-dropdown />
@ -274,8 +274,6 @@ export default {
<design-destroyer
#default="{ mutate, loading }"
:filenames="selectedDesigns"
:project-path="projectPath"
:iid="issueIid"
@done="onDesignDelete"
@error="onDesignDeleteError"
>

View File

@ -1,3 +1,2 @@
export const ROOT_ROUTE_NAME = 'root';
export const DESIGNS_ROUTE_NAME = 'designs';
export const DESIGN_ROUTE_NAME = 'design';

View File

@ -1,4 +1,3 @@
import $ from 'jquery';
import Vue from 'vue';
import VueRouter from 'vue-router';
import routes from './routes';
@ -16,9 +15,7 @@ export default function createRouter(base) {
});
const pageEl = getPageLayoutElement();
router.beforeEach(({ meta: { el }, name }, _, next) => {
$(`#${el}`).tab('show');
router.beforeEach(({ name }, _, next) => {
// apply a fullscreen layout style in Design View (a.k.a design detail)
if (pageEl) {
if (name === DESIGN_ROUTE_NAME) {

View File

@ -1,44 +1,28 @@
import Home from '../pages/index.vue';
import DesignDetail from '../pages/design/index.vue';
import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
export default [
{
name: ROOT_ROUTE_NAME,
name: DESIGNS_ROUTE_NAME,
path: '/',
component: Home,
meta: {
el: 'discussion',
},
},
{
name: DESIGNS_ROUTE_NAME,
path: '/designs',
component: Home,
meta: {
el: 'designs',
},
children: [
name: DESIGN_ROUTE_NAME,
path: '/designs/:id',
component: DesignDetail,
beforeEnter(
{
name: DESIGN_ROUTE_NAME,
path: ':id',
component: DesignDetail,
meta: {
el: 'designs',
},
beforeEnter(
{
params: { id },
},
from,
next,
) {
if (typeof id === 'string') {
next();
}
},
props: ({ params: { id } }) => ({ id }),
params: { id },
},
],
from,
next,
) {
if (typeof id === 'string') {
next();
}
},
props: ({ params: { id } }) => ({ id }),
},
];

View File

@ -70,6 +70,7 @@ export default {
>
<gl-form-select
id="jira-project-select"
data-qa-selector="jira_project_dropdown"
class="mb-2"
:options="jiraProjects"
:state="selectState"
@ -135,7 +136,13 @@ export default {
</gl-form-group>
<div class="footer-block row-content-block d-flex justify-content-between">
<gl-button type="submit" category="primary" variant="success" class="js-no-auto-disable">
<gl-button
type="submit"
category="primary"
variant="success"
class="js-no-auto-disable"
data-qa-selector="jira_issues_import_button"
>
{{ __('Next') }}
</gl-button>
<gl-button :href="issuesPath">{{ __('Cancel') }}</gl-button>

View File

@ -67,11 +67,6 @@ export default {
required: false,
default: false,
},
requirementsAvailable: {
type: Boolean,
required: false,
default: false,
},
visibilityHelpPath: {
type: String,
required: false,
@ -136,7 +131,6 @@ export default {
snippetsAccessLevel: featureAccessLevel.EVERYONE,
pagesAccessLevel: featureAccessLevel.EVERYONE,
metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
requirementsAccessLevel: featureAccessLevel.EVERYONE,
containerRegistryEnabled: true,
lfsEnabled: true,
requestAccessEnabled: true,
@ -239,10 +233,6 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.metricsDashboardAccessLevel,
);
this.requirementsAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
this.requirementsAccessLevel,
);
if (this.pagesAccessLevel === featureAccessLevel.EVERYONE) {
// When from Internal->Private narrow access for only members
this.pagesAccessLevel = featureAccessLevel.PROJECT_MEMBERS;
@ -266,9 +256,6 @@ export default {
this.pagesAccessLevel = featureAccessLevel.EVERYONE;
if (this.metricsDashboardAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
this.metricsDashboardAccessLevel = featureAccessLevel.EVERYONE;
if (this.requirementsAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
this.requirementsAccessLevel = featureAccessLevel.EVERYONE;
this.highlightChanges();
}
},
@ -483,18 +470,6 @@ export default {
/>
</project-setting-row>
</div>
<project-setting-row
v-if="requirementsAvailable"
ref="requirements-settings"
:label="s__('ProjectSettings|Requirements')"
:help-text="s__('ProjectSettings|Requirements management system for this project')"
>
<project-feature-setting
v-model="requirementsAccessLevel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][requirements_access_level]"
/>
</project-setting-row>
<project-setting-row
ref="wiki-settings"
:label="s__('ProjectSettings|Wiki')"

View File

@ -2,7 +2,6 @@ export default {
data() {
return {
packagesEnabled: false,
requirementsEnabled: false,
};
},
watch: {

View File

@ -0,0 +1,72 @@
<script>
import { __ } from '~/locale';
import { GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
/**
* Renders header section with icon and expand button
* Renders expanable content section with grey background
*/
export default {
name: 'MrWidgetExpanableSection',
components: {
GlButton,
GlCollapse,
GlIcon,
},
props: {
iconName: {
type: String,
required: false,
default: 'status_warning',
},
},
data() {
return {
contentIsVisible: false,
};
},
computed: {
collapseButtonText() {
if (this.contentIsVisible) {
return __('Collapse');
}
return __('Expand');
},
},
methods: {
updateContentVisibility() {
this.contentIsVisible = !this.contentIsVisible;
},
},
};
</script>
<template>
<div>
<div class="mr-widget-body gl-display-flex">
<span
class="gl-display-flex gl-align-items-center gl-justify-content-center append-right-default gl-align-self-start gl-mt-1"
>
<gl-icon :name="iconName" :size="24" />
</span>
<div class="gl-display-flex gl-flex-fill-1 gl-flex-direction-column gl-md-flex-direction-row">
<slot name="header"></slot>
<div>
<gl-button @click="updateContentVisibility">
{{ collapseButtonText }}
</gl-button>
</div>
</div>
</div>
<gl-collapse
:visible="contentIsVisible"
class="gl-bg-gray-10 gl-border-t-solid gl-border-gray-100 gl-border-1"
>
<slot name="content"></slot>
</gl-collapse>
</div>
</template>

View File

@ -1,6 +1,8 @@
<script>
import { GlSkeletonLoading } from '@gitlab/ui';
import { n__ } from '~/locale';
import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue';
import Poll from '~/lib/utils/poll';
import TerraformPlan from './terraform_plan.vue';
@ -8,6 +10,8 @@ export default {
name: 'MRWidgetTerraformContainer',
components: {
GlSkeletonLoading,
GlSprintf,
MrWidgetExpanableSection,
TerraformPlan,
},
props: {
@ -19,10 +23,43 @@ export default {
data() {
return {
loading: true,
plans: {},
plansObject: {},
poll: null,
};
},
computed: {
inValidPlanCountText() {
if (this.numberOfInvalidPlans === 0) {
return null;
}
return n__(
'Terraform|%{number} Terraform report failed to generate',
'Terraform|%{number} Terraform reports failed to generate',
this.numberOfInvalidPlans,
);
},
numberOfInvalidPlans() {
return Object.values(this.plansObject).filter(plan => plan.tf_report_error).length;
},
numberOfPlans() {
return Object.keys(this.plansObject).length;
},
numberOfValidPlans() {
return this.numberOfPlans - this.numberOfInvalidPlans;
},
validPlanCountText() {
if (this.numberOfValidPlans === 0) {
return null;
}
return n__(
'Terraform|%{number} Terraform report was generated in your pipelines',
'Terraform|%{number} Terraform reports were generated in your pipelines',
this.numberOfValidPlans,
);
},
},
created() {
this.fetchPlans();
},
@ -40,15 +77,15 @@ export default {
data: this.endpoint,
method: 'fetchPlans',
successCallback: ({ data }) => {
this.plans = data;
this.plansObject = data;
if (Object.keys(this.plans).length) {
if (this.numberOfPlans > 0) {
this.loading = false;
this.poll.stop();
}
},
errorCallback: () => {
this.plans = { bad_plan: {} };
this.plansObject = { bad_plan: { tf_report_error: 'api_error' } };
this.loading = false;
this.poll.stop();
},
@ -62,16 +99,42 @@ export default {
<template>
<section class="mr-widget-section">
<div v-if="loading" class="mr-widget-body media">
<div v-if="loading" class="mr-widget-body">
<gl-skeleton-loading />
</div>
<terraform-plan
v-for="(plan, key) in plans"
v-else
:key="key"
:plan="plan"
class="mr-widget-body media"
/>
<mr-widget-expanable-section v-else>
<template #header>
<div
data-testid="terraform-header-text"
class="gl-flex-fill-1 gl-display-flex gl-flex-direction-column"
>
<p v-if="validPlanCountText" class="gl-m-0">
<gl-sprintf :message="validPlanCountText">
<template #number>
<strong>{{ numberOfValidPlans }}</strong>
</template>
</gl-sprintf>
</p>
<p v-if="inValidPlanCountText" class="gl-m-0">
<gl-sprintf :message="inValidPlanCountText">
<template #number>
<strong>{{ numberOfInvalidPlans }}</strong>
</template>
</gl-sprintf>
</p>
</div>
</template>
<template #content>
<terraform-plan
v-for="(plan, key) in plansObject"
:key="key"
:plan="plan"
class="mr-widget-body"
/>
</template>
</mr-widget-expanable-section>
</section>
</template>

View File

@ -25,21 +25,28 @@ export default {
deleteNum() {
return Number(this.plan.delete);
},
iconType() {
return this.validPlanValues ? 'doc-changes' : 'warning';
},
reportChangeText() {
if (this.validPlanValues) {
return __(
'Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
);
}
return __('Generating the report caused an error.');
return __('Terraform|Generating the report caused an error.');
},
reportHeaderText() {
if (this.plan.job_name) {
return __('The Terraform report %{name} was generated in your pipelines.');
if (this.validPlanValues) {
return this.plan.job_name
? __('Terraform|The Terraform report %{name} was generated in your pipelines.')
: __('Terraform|A Terraform report was generated in your pipelines.');
}
return __('A Terraform report was generated in your pipelines.');
return this.plan.job_name
? __('Terraform|The Terraform report %{name} failed to generate.')
: __('Terraform|A Terraform report failed to generate.');
},
validPlanValues() {
return this.addNum + this.changeNum + this.deleteNum >= 0;
@ -53,11 +60,11 @@ export default {
<span
class="gl-display-flex gl-align-items-center gl-justify-content-center append-right-default gl-align-self-start gl-mt-1"
>
<gl-icon name="status_warning" :size="24" />
<gl-icon :name="iconType" :size="18" data-testid="change-type-icon" />
</span>
<div class="gl-display-flex gl-flex-fill-1 gl-flex-direction-column flex-md-row">
<div class="terraform-mr-plan-text normal gl-display-flex gl-flex-direction-column">
<div class="gl-flex-fill-1 gl-display-flex gl-flex-direction-column">
<p class="gl-m-0 gl-pr-1">
<gl-sprintf :message="reportHeaderText">
<template #name>
@ -88,10 +95,11 @@ export default {
v-if="plan.job_path"
:href="plan.job_path"
target="_blank"
data-testid="terraform-report-link"
data-track-event="click_terraform_mr_plan_button"
data-track-label="mr_widget_terraform_mr_plan_button"
data-track-property="terraform_mr_plan_button"
class="btn btn-sm js-terraform-report-link"
class="btn btn-sm"
rel="noopener"
>
{{ __('View full log') }}

View File

@ -43,3 +43,7 @@ export const EDITOR_TYPES = {
export const EDITOR_HEIGHT = '100%';
export const EDITOR_PREVIEW_STYLE = 'horizontal';
export const IMAGE_TABS = { UPLOAD_TAB: 0, URL_TAB: 1 };
export const MAX_FILE_SIZE = 2097152; // 2Mb

View File

@ -0,0 +1,137 @@
<script>
import { isSafeURL } from '~/lib/utils/url_utility';
import { GlModal, GlFormGroup, GlFormInput, GlTabs, GlTab } from '@gitlab/ui';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IMAGE_TABS } from '../../constants';
import UploadImageTab from './upload_image_tab.vue';
export default {
components: {
UploadImageTab,
GlModal,
GlFormGroup,
GlFormInput,
GlTabs,
GlTab,
},
mixins: [glFeatureFlagMixin()],
data() {
return {
urlError: null,
imageUrl: null,
description: null,
tabIndex: IMAGE_TABS.UPLOAD_TAB,
uploadImageTab: null,
};
},
modalTitle: __('Image Details'),
okTitle: __('Insert'),
urlTabTitle: __('By URL'),
urlLabel: __('Image URL'),
descriptionLabel: __('Description'),
uploadTabTitle: __('Upload file'),
computed: {
altText() {
return this.description;
},
},
methods: {
show() {
this.urlError = null;
this.imageUrl = null;
this.description = null;
this.tabIndex = IMAGE_TABS.UPLOAD_TAB;
this.$refs.modal.show();
},
onOk(event) {
if (this.glFeatures.sseImageUploads && this.tabIndex === IMAGE_TABS.UPLOAD_TAB) {
this.submitFile(event);
return;
}
this.submitURL(event);
},
setFile(file) {
this.file = file;
},
submitFile(event) {
const { file, altText } = this;
const { uploadImageTab } = this.$refs;
uploadImageTab.validateFile();
if (uploadImageTab.fileError) {
event.preventDefault();
return;
}
this.$emit('addImage', { file, altText: altText || file.name });
},
submitURL(event) {
if (!this.validateUrl()) {
event.preventDefault();
return;
}
const { imageUrl, altText } = this;
this.$emit('addImage', { imageUrl, altText: altText || imageUrl });
},
validateUrl() {
if (!isSafeURL(this.imageUrl)) {
this.urlError = __('Please provide a valid URL');
this.$refs.urlInput.$el.focus();
return false;
}
return true;
},
},
};
</script>
<template>
<gl-modal
ref="modal"
modal-id="add-image-modal"
:title="$options.modalTitle"
:ok-title="$options.okTitle"
@ok="onOk"
>
<gl-tabs v-if="glFeatures.sseImageUploads" v-model="tabIndex">
<!-- Upload file Tab -->
<gl-tab :title="$options.uploadTabTitle">
<upload-image-tab ref="uploadImageTab" @input="setFile" />
</gl-tab>
<!-- By URL Tab -->
<gl-tab :title="$options.urlTabTitle">
<gl-form-group
class="gl-mt-5 gl-mb-3"
:label="$options.urlLabel"
label-for="url-input"
:state="!Boolean(urlError)"
:invalid-feedback="urlError"
>
<gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" />
</gl-form-group>
</gl-tab>
</gl-tabs>
<gl-form-group
v-else
class="gl-mt-5 gl-mb-3"
:label="$options.urlLabel"
label-for="url-input"
:state="!Boolean(urlError)"
:invalid-feedback="urlError"
>
<gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" />
</gl-form-group>
<!-- Description Input -->
<gl-form-group :label="$options.descriptionLabel" label-for="description-input">
<gl-form-input id="description-input" ref="descriptionInput" v-model="description" />
</gl-form-group>
</gl-modal>
</template>

View File

@ -0,0 +1,56 @@
<script>
import { __ } from '~/locale';
import { GlFormGroup } from '@gitlab/ui';
import { MAX_FILE_SIZE } from '../../constants';
export default {
components: {
GlFormGroup,
},
data() {
return {
file: null,
fileError: null,
};
},
fileLabel: __('Select file'),
methods: {
onInput(event) {
[this.file] = event.target.files;
this.validateFile();
if (!this.fileError) {
this.$emit('input', this.file);
}
},
validateFile() {
this.fileError = null;
if (!this.file) {
this.fileError = __('Please choose a file');
} else if (this.file.size > MAX_FILE_SIZE) {
this.fileError = __('Maximum file size is 2MB. Please select a smaller file.');
}
},
},
};
</script>
<template>
<gl-form-group
class="gl-mt-5 gl-mb-3"
:label="$options.fileLabel"
label-for="file-input"
:state="!Boolean(fileError)"
:invalid-feedback="fileError"
>
<input
id="file-input"
ref="fileInput"
class="gl-mt-3 gl-mb-2"
type="file"
accept="image/*"
@input="onInput"
/>
</gl-form-group>
</template>

View File

@ -1,74 +0,0 @@
<script>
import { isSafeURL } from '~/lib/utils/url_utility';
import { GlModal, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlModal,
GlFormGroup,
GlFormInput,
},
data() {
return {
error: null,
imageUrl: null,
altText: null,
modalTitle: __('Image Details'),
okTitle: __('Insert'),
urlLabel: __('Image URL'),
descriptionLabel: __('Description'),
};
},
methods: {
show() {
this.error = null;
this.imageUrl = null;
this.altText = null;
this.$refs.modal.show();
},
onOk(event) {
if (!this.isValid()) {
event.preventDefault();
return;
}
const { imageUrl, altText } = this;
this.$emit('addImage', { imageUrl, altText: altText || __('image') });
},
isValid() {
if (!isSafeURL(this.imageUrl)) {
this.error = __('Please provide a valid URL');
this.$refs.urlInput.$el.focus();
return false;
}
return true;
},
},
};
</script>
<template>
<gl-modal
ref="modal"
modal-id="add-image-modal"
:title="modalTitle"
:ok-title="okTitle"
@ok="onOk"
>
<gl-form-group
:label="urlLabel"
label-for="url-input"
:state="!Boolean(error)"
:invalid-feedback="error"
>
<gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" />
</gl-form-group>
<gl-form-group :label="descriptionLabel" label-for="description-input">
<gl-form-input id="description-input" ref="descriptionInput" v-model="altText" />
</gl-form-group>
</gl-modal>
</template>

View File

@ -2,7 +2,7 @@
import 'codemirror/lib/codemirror.css';
import '@toast-ui/editor/dist/toastui-editor.css';
import AddImageModal from './modals/add_image_modal.vue';
import AddImageModal from './modals/add_image/add_image_modal.vue';
import {
EDITOR_OPTIONS,
EDITOR_TYPES,
@ -18,6 +18,8 @@ import {
getMarkdown,
} from './services/editor_service';
import { getUrl } from './services/image_service';
export default {
components: {
ToastEditor: () =>
@ -96,7 +98,16 @@ export default {
onOpenAddImageModal() {
this.$refs.addImageModal.show();
},
onAddImage(image) {
onAddImage({ imageUrl, altText, file }) {
const image = { imageUrl, altText };
if (file) {
image.imageUrl = getUrl(file);
// TODO - persist images locally (local image repository)
// TODO - ensure that the actual repo URL for the image is used in Markdown mode
// TODO - upload images to the project repository (on submit)
}
addImage(this.editorInstance, image);
},
onChangeMode(newMode) {

View File

@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export const getUrl = file => URL.createObjectURL(file);

View File

@ -9,6 +9,9 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
prepend_before_action :authenticate_user!, only: [:show]
before_action :assign_ref_and_path, only: [:show]
before_action :authorize_edit_tree!, only: [:show]
before_action do
push_frontend_feature_flag(:sse_image_uploads)
end
def show
@config = Gitlab::StaticSiteEditor::Config.new(@repository, @ref, @path, params[:return_url])

View File

@ -356,20 +356,6 @@ class ProjectsController < Projects::ApplicationController
.merge(import_url_params)
end
def project_feature_attributes
%i[
builds_access_level
issues_access_level
forking_access_level
merge_requests_access_level
repository_access_level
snippets_access_level
wiki_access_level
pages_access_level
metrics_dashboard_access_level
]
end
def project_params_attributes
[
:allow_merge_on_skipped_pipeline,
@ -405,10 +391,22 @@ class ProjectsController < Projects::ApplicationController
:initialize_with_readme,
:autoclose_referenced_issues,
:suggestion_commit_message,
project_feature_attributes: %i[
builds_access_level
issues_access_level
forking_access_level
merge_requests_access_level
repository_access_level
snippets_access_level
wiki_access_level
pages_access_level
metrics_dashboard_access_level
],
project_setting_attributes: %i[
show_default_award_emojis
]
] + [project_feature_attributes: project_feature_attributes]
]
end
def project_params_create_attributes

View File

@ -91,6 +91,12 @@ module Types
null: true,
description: 'Assignees of the alert'
field :metrics_dashboard_url,
GraphQL::STRING_TYPE,
null: true,
description: 'URL for metrics embed for the alert',
resolve: -> (alert, _args, _context) { alert.present.metrics_dashboard_url }
def notes
object.ordered_notes
end

View File

@ -40,7 +40,8 @@ module Ci
cobertura: 'cobertura-coverage.xml',
terraform: 'tfplan.json',
cluster_applications: 'gl-cluster-applications.json',
requirements: 'requirements.json'
requirements: 'requirements.json',
coverage_fuzzing: 'gl-coverage-fuzzing.json'
}.freeze
INTERNAL_TYPES = {
@ -73,7 +74,8 @@ module Ci
license_scanning: :raw,
performance: :raw,
terraform: :raw,
requirements: :raw
requirements: :raw,
coverage_fuzzing: :raw
}.freeze
DOWNLOADABLE_TYPES = %w[
@ -187,7 +189,8 @@ module Ci
accessibility: 19,
cluster_applications: 20,
secret_detection: 21, ## EE-specific
requirements: 22 ## EE-specific
requirements: 22, ## EE-specific
coverage_fuzzing: 23 ## EE-specific
}
enum file_format: {

View File

@ -37,8 +37,7 @@ module Featurable
class_methods do
def set_available_features(available_features = [])
@available_features ||= []
@available_features += available_features
@available_features = available_features
class_eval do
available_features.each do |feature|

View File

@ -88,5 +88,3 @@ module ProjectFeaturesCompatibility
project_feature.__send__(:write_attribute, field, value) # rubocop:disable GitlabSecurity/PublicSend
end
end
ProjectsHelper.prepend_if_ee('EE::ProjectFeaturesCompatibility')

View File

@ -21,6 +21,8 @@ class MergeRequest < ApplicationRecord
include MilestoneEventable
include StateEventable
extend ::Gitlab::Utils::Override
sha_attribute :squash_commit_sha
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
@ -1582,6 +1584,23 @@ class MergeRequest < ApplicationRecord
super.merge(label_url_method: :project_merge_requests_url)
end
override :ensure_metrics
def ensure_metrics
MergeRequest::Metrics.safe_find_or_create_by(merge_request_id: id).tap do |metrics_record|
# Make sure we refresh the loaded association object with the newly created/loaded item.
# This is needed in order to have the exact functionality than before.
#
# Example:
#
# merge_request.metrics.destroy
# merge_request.ensure_metrics
# merge_request.metrics # should return the metrics record and not nil
# merge_request.metrics.merge_request # should return the same MR record
metrics_record.association(:merge_request).target = self
association(:metrics).target = metrics_record
end
end
private
def with_rebase_lock

View File

@ -37,6 +37,8 @@ module AlertManagement
MARKDOWN
end
def metrics_dashboard_url; end
private
attr_reader :alert, :project

View File

@ -2,6 +2,10 @@
module AlertManagement
class PrometheusAlertPresenter < AlertManagement::AlertPresenter
def metrics_dashboard_url
alerting_alert.metrics_dashboard_url
end
private
def alert_markdown

View File

@ -68,9 +68,13 @@ module Projects
end
def metric_embed_for_alert
url = embed_url_for_gitlab_alert || embed_url_for_self_managed_alert
"\n[](#{metrics_dashboard_url})" if metrics_dashboard_url
end
"\n[](#{url})" if url
def metrics_dashboard_url
strong_memoize(:metrics_dashboard_url) do
embed_url_for_gitlab_alert || embed_url_for_self_managed_alert
end
end
private
@ -133,6 +137,7 @@ module Projects
project,
gitlab_alert.prometheus_metric_id,
environment_id: environment.id,
embedded: true,
**alert_embed_window_params(embed_time)
)
end
@ -144,6 +149,7 @@ module Projects
project,
environment,
embed_json: dashboard_for_self_managed_alert.to_json,
embedded: true,
**alert_embed_window_params(embed_time)
)
end

View File

@ -27,6 +27,11 @@ module Snippets
attempt_destroy!
# Update project statistics if the snippet is a Project one
if snippet.project_id
ProjectCacheWorker.perform_async(snippet.project_id, [], [:snippets_size])
end
ServiceResponse.success(message: 'Snippet was deleted.')
rescue DestroyError
service_response_error('Failed to remove snippet repository.', 400)

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Snippets
class UpdateStatisticsService
attr_reader :snippet
def initialize(snippet)
@snippet = snippet
end
def execute
unless snippet.repository_exists?
return ServiceResponse.error(message: 'Invalid snippet repository', http_status: 400)
end
snippet.repository.expire_statistics_caches
statistics.refresh!
# Update project statistics if the snippet is a Project one
if snippet.project_id
ProjectCacheWorker.perform_async(snippet.project_id, [], [:snippets_size])
end
ServiceResponse.success(message: 'Snippet statistics successfully updated.')
end
private
def statistics
@statistics ||= snippet.statistics || snippet.build_statistics
end
end
end

View File

@ -3,7 +3,7 @@
.dropdown.btn-group
%button.btn.rounded-right.text-center{ class: ('has-tooltip' if type == :icon), title: (_('Import issues') if type == :icon),
data: { toggle: 'dropdown' }, 'aria-label' => _('Import issues'), 'aria-haspopup' => 'true', 'aria-expanded' => 'false' }
data: { toggle: 'dropdown', qa_selector: 'import_issues_button' }, 'aria-label' => _('Import issues'), 'aria-haspopup' => 'true', 'aria-expanded' => 'false' }
- if type == :icon
= sprite_icon('import')
- else
@ -13,4 +13,5 @@
%button{ data: { toggle: 'modal', target: '.issues-import-modal' } }
= _('Import CSV')
- if can_edit
%li= link_to _('Import from Jira'), project_import_jira_path(@project)
%li{ data: { qa_selector: 'import_from_jira_link' } }
= link_to _('Import from Jira'), project_import_jira_path(@project)

View File

@ -77,6 +77,9 @@
- if @issue.sentry_issue.present?
#js-sentry-error-stack-trace{ data: error_details_data(@project, @issue.sentry_issue.sentry_issue_identifier) }
- if Feature.enabled?(:design_management_moved, @project)
= render 'projects/issues/design_management'
= render_if_exists 'projects/issues/related_issues'
#js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
@ -94,6 +97,9 @@
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' if show_new_branch_button?
= render 'projects/issues/tabs'
- if Feature.enabled?(:design_management_moved, @project)
= render 'projects/issues/discussion'
- else
= render 'projects/issues/tabs'
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees

View File

@ -79,7 +79,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
return false unless user
expire_caches(post_received, snippet.repository)
snippet.repository.expire_statistics_caches
Snippets::UpdateStatisticsService.new(snippet).execute
end
# Expire the repository status, branch, and tag cache once per push.

View File

@ -0,0 +1,5 @@
---
title: Add expand/collapse view to Terraform MR widget
merge_request: 34879
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: "Added support for reordering issues to the v4 API"
merge_request: 35349
author: Joel @jjshoe, Lee Tickett @leetickett
type: added

View File

@ -1,5 +0,0 @@
---
title: Add requirements visibility/access project settings
merge_request: 34420
author: Lee Tickett
type: added

View File

@ -0,0 +1,5 @@
---
title: Deduplicate merge_request_metrics table
merge_request: 29566
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Disable ILM on ELK vendor yaml
merge_request: 35398
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Make logrotate run as git user for source installations
merge_request: 35519
author:
type: security

View File

@ -0,0 +1,5 @@
---
title: Update snippet and project statistics after certain events
merge_request: 35340
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Expose metrics dashboard URL for alert GraphQL query
merge_request: 35293
author:
type: added

View File

@ -1,19 +0,0 @@
# frozen_string_literal: true
class AddRequirementsAccessLevelToProjectFeatures < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :project_features, :requirements_access_level, :integer, default: 20, null: false
end
end
def down
with_lock_retries do
remove_column :project_features, :requirements_access_level, :integer
end
end
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
class DedupMrMetrics < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
TMP_INDEX_NAME = 'tmp_unique_merge_request_metrics_by_merge_request_id'
INDEX_NAME = 'unique_merge_request_metrics_by_merge_request_id'
disable_ddl_transaction!
class MergeRequestMetrics < ActiveRecord::Base
self.table_name = 'merge_request_metrics'
include EachBatch
end
def up
last_metrics_record_id = MergeRequestMetrics.maximum(:id) || 0
# This index will disallow further duplicates while we're deduplicating the data.
add_concurrent_index(:merge_request_metrics, :merge_request_id, where: "id > #{Integer(last_metrics_record_id)}", unique: true, name: TMP_INDEX_NAME)
MergeRequestMetrics.each_batch do |relation|
duplicated_merge_request_ids = MergeRequestMetrics
.where(merge_request_id: relation.select(:merge_request_id))
.select(:merge_request_id)
.group(:merge_request_id)
.having('COUNT(merge_request_metrics.merge_request_id) > 1')
.pluck(:merge_request_id)
duplicated_merge_request_ids.each do |merge_request_id|
deduplicate_item(merge_request_id)
end
end
add_concurrent_index(:merge_request_metrics, :merge_request_id, unique: true, name: INDEX_NAME)
remove_concurrent_index_by_name(:merge_request_metrics, TMP_INDEX_NAME)
end
def down
remove_concurrent_index_by_name(:merge_request_metrics, TMP_INDEX_NAME)
remove_concurrent_index_by_name(:merge_request_metrics, INDEX_NAME)
end
private
def deduplicate_item(merge_request_id)
merge_request_metrics_records = MergeRequestMetrics.where(merge_request_id: merge_request_id).order(updated_at: :asc).to_a
attributes = {}
merge_request_metrics_records.each do |merge_request_metrics_record|
params = merge_request_metrics_record.attributes.except('id')
attributes.merge!(params.compact)
end
ActiveRecord::Base.transaction do
record_to_keep = merge_request_metrics_records.pop
records_to_delete = merge_request_metrics_records
MergeRequestMetrics.where(id: records_to_delete.map(&:id)).delete_all
record_to_keep.update!(attributes)
end
end
end

View File

@ -13976,8 +13976,7 @@ CREATE TABLE public.project_features (
repository_access_level integer DEFAULT 20 NOT NULL,
pages_access_level integer NOT NULL,
forking_access_level integer,
metrics_dashboard_access_level integer,
requirements_access_level integer DEFAULT 20 NOT NULL
metrics_dashboard_access_level integer
);
CREATE SEQUENCE public.project_features_id_seq
@ -20416,6 +20415,8 @@ CREATE INDEX tmp_index_ci_pipelines_lock_version ON public.ci_pipelines USING bt
CREATE INDEX tmp_index_ci_stages_lock_version ON public.ci_stages USING btree (id) WHERE (lock_version IS NULL);
CREATE UNIQUE INDEX unique_merge_request_metrics_by_merge_request_id ON public.merge_request_metrics USING btree (merge_request_id);
CREATE UNIQUE INDEX users_security_dashboard_projects_unique_index ON public.users_security_dashboard_projects USING btree (project_id, user_id);
CREATE UNIQUE INDEX vulnerability_feedback_unique_idx ON public.vulnerability_feedback USING btree (project_id, category, feedback_type, project_fingerprint);
@ -23410,6 +23411,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200525144525
20200526000407
20200526013844
20200526115436
20200526120714
20200526142550
20200526153844
@ -23459,7 +23461,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200615121217
20200615123055
20200615193524
20200615203153
20200615232735
20200615234047
20200616145031

View File

@ -1,20 +1,5 @@
---
type: reference
redirect_to: '../postgresql/index.md'
---
# Configuring PostgreSQL for Scaling and High Availability
In this section, you'll be guided through configuring a PostgreSQL database to
be used with GitLab in one of our [Scalable and Highly Available Setups](../reference_architectures/index.md).
## Provide your own PostgreSQL instance **(CORE ONLY)**
This content has been moved to a [new location](../postgresql/external.md).
## Standalone PostgreSQL using Omnibus GitLab **(CORE ONLY)**
This content has been moved to a [new location](../postgresql/standalone.md).
## PostgreSQL replication and failover with Omnibus GitLab **(PREMIUM ONLY)**
This content has been moved to a [new location](../postgresql/replication_and_failover.md).
This document was moved to [another location](../postgresql/index.md).

View File

@ -0,0 +1,36 @@
---
type: reference
---
# Configuring PostgreSQL for scaling
In this section, you'll be guided through configuring a PostgreSQL database to
be used with GitLab in one of our [Scalable and Highly Available Setups](../reference_architectures/index.md).
There are essentially three setups to choose from.
## PostgreSQL replication and failover with Omnibus GitLab **(PREMIUM ONLY)**
This setup is for when you have installed GitLab using the
[Omnibus GitLab **Enterprise Edition** (EE) package](https://about.gitlab.com/install/?version=ee).
All the tools that are needed like PostgreSQL, PgBouncer, Repmgr are bundled in
the package, so you can it to set up the whole PostgreSQL infrastructure (primary, replica).
[> Read how to set up PostgreSQL replication and failover using Omnibus GitLab](replication_and_failover.md)
## Standalone PostgreSQL using Omnibus GitLab **(CORE ONLY)**
This setup is for when you have installed the
[Omnibus GitLab packages](https://about.gitlab.com/install/) (CE or EE),
to use the bundled PostgreSQL having only its service enabled.
[> Read how to set up a standalone PostgreSQL instance using Omnibus GitLab](standalone.md)
## Provide your own PostgreSQL instance **(CORE ONLY)**
This setup is for when you have installed GitLab using the
[Omnibus GitLab packages](https://about.gitlab.com/install/) (CE or EE),
or installed it [from source](../../install/installation.md), but you want to use
your own external PostgreSQL server.
[> Read how to set up an external PostgreSQL instance](external.md)

View File

@ -1,16 +1,15 @@
# PostgreSQL replication and failover with Omnibus GitLab **(PREMIUM ONLY)**
> Important notes:
>
> - This document will focus only on configuration supported with [GitLab Premium](https://about.gitlab.com/pricing/), using the Omnibus GitLab package.
> - If you are a Community Edition or Starter user, consider using a cloud hosted solution.
> - This document will not cover installations from source.
>
> - If a setup with replication and failover is not what you were looking for, see the [database configuration document](https://docs.gitlab.com/omnibus/settings/database.html)
> for the Omnibus GitLab packages.
>
> Please read this document fully before attempting to configure PostgreSQL with
> replication and failover for GitLab.
This document will focus only on configuration supported with [GitLab Premium](https://about.gitlab.com/pricing/), using the Omnibus GitLab package.
If you are a Community Edition or Starter user, consider using a cloud hosted solution.
This document will not cover installations from source.
If a setup with replication and failover is not what you were looking for, see
the [database configuration document](https://docs.gitlab.com/omnibus/settings/database.html)
for the Omnibus GitLab packages.
It's recommended to read this document fully before attempting to configure PostgreSQL with
replication and failover for GitLab.
## Architecture

View File

@ -259,6 +259,11 @@ type AlertManagementAlert implements Noteable {
"""
issueIid: ID
"""
URL for metrics embed for the alert
"""
metricsDashboardUrl: String
"""
Monitoring tool the alert came from
"""
@ -11309,6 +11314,11 @@ type SecurityReportSummary {
"""
containerScanning: SecurityReportSummarySection
"""
Aggregated counts for the coverage_fuzzing scan
"""
coverageFuzzing: SecurityReportSummarySection
"""
Aggregated counts for the dast scan
"""
@ -13994,7 +14004,8 @@ type Vulnerability {
"""
Type of the security report that found the vulnerability (SAST,
DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION)
DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION,
COVERAGE_FUZZING)
"""
reportType: VulnerabilityReportType
@ -14332,6 +14343,7 @@ The type of the security scan that found the vulnerability.
"""
enum VulnerabilityReportType {
CONTAINER_SCANNING
COVERAGE_FUZZING
DAST
DEPENDENCY_SCANNING
SAST

View File

@ -716,6 +716,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "metricsDashboardUrl",
"description": "URL for metrics embed for the alert",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "monitoringTool",
"description": "Monitoring tool the alert came from",
@ -33204,6 +33218,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "coverageFuzzing",
"description": "Aggregated counts for the coverage_fuzzing scan",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "SecurityReportSummarySection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dast",
"description": "Aggregated counts for the dast scan",
@ -41227,7 +41255,7 @@
},
{
"name": "reportType",
"description": "Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION)",
"description": "Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING)",
"args": [
],
@ -42297,6 +42325,12 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "COVERAGE_FUZZING",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null

View File

@ -69,6 +69,7 @@ Describes an alert from the project's Alert Management
| `hosts` | String! => Array | List of hosts the alert came from |
| `iid` | ID! | Internal ID of the alert |
| `issueIid` | ID | Internal ID of the GitLab issue attached to the alert |
| `metricsDashboardUrl` | String | URL for metrics embed for the alert |
| `monitoringTool` | String | Monitoring tool the alert came from |
| `service` | String | Service the alert came from |
| `severity` | AlertManagementSeverity | Severity of the alert |
@ -1642,6 +1643,7 @@ Represents summary of a security report
| Name | Type | Description |
| --- | ---- | ---------- |
| `containerScanning` | SecurityReportSummarySection | Aggregated counts for the container_scanning scan |
| `coverageFuzzing` | SecurityReportSummarySection | Aggregated counts for the coverage_fuzzing scan |
| `dast` | SecurityReportSummarySection | Aggregated counts for the dast scan |
| `dependencyScanning` | SecurityReportSummarySection | Aggregated counts for the dependency_scanning scan |
| `sast` | SecurityReportSummarySection | Aggregated counts for the sast scan |
@ -2100,7 +2102,7 @@ Represents a vulnerability.
| `location` | VulnerabilityLocation | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability |
| `primaryIdentifier` | VulnerabilityIdentifier | Primary identifier of the vulnerability. |
| `project` | Project | The project on which the vulnerability was found |
| `reportType` | VulnerabilityReportType | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION) |
| `reportType` | VulnerabilityReportType | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING) |
| `scanner` | VulnerabilityScanner | Scanner metadata for the vulnerability. |
| `severity` | VulnerabilitySeverity | Severity of the vulnerability (INFO, UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL) |
| `state` | VulnerabilityState | State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED) |

View File

@ -901,6 +901,25 @@ DELETE /projects/:id/issues/:issue_iid
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/issues/85"
```
## Reorder an issue
Reorders an issue, you can see the results when sorting issues manually
```plaintext
PUT /projects/:id/issues/:issue_iid/reorder
```
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
| `move_after_id` | integer | no | The ID of a projet's issue to move this issue after |
| `move_before_id` | integer | no | The ID of a projet's issue to move this issue before |
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/issues/85/reorder?move_after_id=51&move_before_id=92"
```
## Move an issue
Moves an issue to a different project. If the target project

View File

@ -1048,7 +1048,6 @@ POST /projects
| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `requirements_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `emails_disabled` | boolean | no | Disable email notifications |
| `show_default_award_emojis` | boolean | no | Show default award emojis |
| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
@ -1120,7 +1119,6 @@ POST /projects/user/:user_id
| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `requirements_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `emails_disabled` | boolean | no | Disable email notifications |
| `show_default_award_emojis` | boolean | no | Show default award emojis |
| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
@ -1191,7 +1189,6 @@ PUT /projects/:id
| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `requirements_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `emails_disabled` | boolean | no | Disable email notifications |
| `show_default_award_emojis` | boolean | no | Show default award emojis |
| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |

View File

@ -30,7 +30,7 @@ subgraph "2. gitlab `review-prepare` stage"
end
subgraph "3. gitlab `review` stage"
C["review-deploy<br><br>Helm deploys the Review App using the Cloud<br/>Native images built by the CNG-mirror pipeline.<br><br>Cloud Native images are deployed to the `review-apps-ce` or `review-apps-ee`<br>Kubernetes (GKE) cluster, in the GCP `gitlab-review-apps` project."]
C["review-deploy<br><br>Helm deploys the Review App using the Cloud<br/>Native images built by the CNG-mirror pipeline.<br><br>Cloud Native images are deployed to the `review-apps`<br>Kubernetes (GKE) cluster, in the GCP `gitlab-review-apps` project."]
end
subgraph "4. gitlab `qa` stage"
@ -62,7 +62,7 @@ subgraph "CNG-mirror pipeline"
job, which runs only for tags, and triggers itself a [`CNG`](https://gitlab.com/gitlab-org/build/CNG) pipeline.
1. Once the `test` stage is done, the [`review-deploy`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724810) job
deploys the Review App using [the official GitLab Helm chart](https://gitlab.com/gitlab-org/charts/gitlab/) to
the [`review-apps-ce`](https://console.cloud.google.com/kubernetes/clusters/details/us-central1-a/review-apps-ce?project=gitlab-review-apps) / [`review-apps-ee`](https://console.cloud.google.com/kubernetes/clusters/details/us-central1-b/review-apps-ee?project=gitlab-review-apps)
the [`review-apps`](https://console.cloud.google.com/kubernetes/clusters/details/us-central1-b/review-apps?project=gitlab-review-apps)
Kubernetes cluster on GCP.
- The actual scripts used to deploy the Review App can be found at
[`scripts/review_apps/review-apps.sh`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/review_apps/review-apps.sh).
@ -136,11 +136,10 @@ browser performance testing using a
### Node pools
The `review-apps-ee` and `review-apps-ce` clusters are currently set up with
The `review-apps` cluster is currently set up with
the following node pools:
- `review-apps-ee` of pre-emptible `e2-highcpu-16` (16 vCPU, 16 GB memory) nodes with autoscaling
- `review-apps-ce` of pre-emptible `n1-standard-8` (8 vCPU, 16 GB memory) nodes with autoscaling
- `e2-highcpu-16` (16 vCPU, 16 GB memory) pre-emptible nodes with autoscaling
### Helm
@ -189,9 +188,7 @@ secure note named `gitlab-{ce,ee} Review App's root password`.
1. Click on the `KUBECTL` dropdown, then `Exec` -> `task-runner`.
1. Replace `-c task-runner -- ls` with `-it -- gitlab-rails console` from the
default command or
- Run `kubectl exec --namespace review-apps-ce review-qa-raise-e-12chm0-task-runner-d5455cc8-2lsvz -it -- gitlab-rails console` and
- Replace `review-apps-ce` with `review-apps-ee` if the Review App
is running EE, and
- Run `kubectl exec --namespace review-apps review-qa-raise-e-12chm0-task-runner-d5455cc8-2lsvz -it -- gitlab-rails console` and
- Replace `review-qa-raise-e-12chm0-task-runner-d5455cc8-2lsvz`
with your Pod's name.

View File

@ -23,12 +23,14 @@ Enable code intelligence for a project by adding a GitLab CI/CD job to the proje
```yaml
code_navigation:
image: golang:1.14.0
allow_failure: true # recommended
script:
- go get github.com/sourcegraph/lsif-go/cmd/lsif-go
- lsif-go
artifacts:
reports:
lsif: dump.lsif
artifacts:
reports:
lsif: dump.lsif
```
The generated LSIF file must be less than 170MiB.

View File

@ -62,7 +62,6 @@ Use the switches to enable or disable the following features:
| **Snippets** | ✓ | Enables [sharing of code and text](../../snippets.md) |
| **Pages** | ✓ | Allows you to [publish static websites](../pages/) |
| **Metrics Dashboard** | ✓ | Control access to [metrics dashboard](../integrations/prometheus.md)
| **Requirements** | ✓ | Control access to [Requirements Management](../requirements/index.md)
Some features depend on others:

View File

@ -289,6 +289,30 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Reorder an existing issue' do
success Entities::Issue
end
params do
requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
optional :move_after_id, type: Integer, desc: 'The ID of the issue we want to be after'
optional :move_before_id, type: Integer, desc: 'The ID of the issue we want to be before'
at_least_one_of :move_after_id, :move_before_id
end
# rubocop: disable CodeReuse/ActiveRecord
put ':id/issues/:issue_iid/reorder' do
issue = user_project.issues.find_by(iid: params[:issue_iid])
not_found!('Issue') unless issue
authorize! :update_issue, issue
if ::Issues::ReorderService.new(user_project, current_user, params).execute(issue)
present issue, with: Entities::Issue, current_user: current_user, project: user_project
else
render_api_error!({ error: 'Unprocessable Entity' }, 422)
end
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Move an existing issue' do
success Entities::Issue
end

View File

@ -15,7 +15,7 @@ module Gitlab
%i[junit codequality sast secret_detection dependency_scanning container_scanning
dast performance license_management license_scanning metrics lsif
dotenv cobertura terraform accessibility cluster_applications
requirements].freeze
requirements coverage_fuzzing].freeze
attributes ALLOWED_KEYS
@ -25,7 +25,8 @@ module Gitlab
with_options allow_nil: true do
validates :junit, array_of_strings_or_string: true
validates :codequality, array_of_strings_or_string: true
validates :coverage_fuzzing, array_of_strings_or_string: true
validates :sast, array_of_strings_or_string: true
validates :sast, array_of_strings_or_string: true
validates :secret_detection, array_of_strings_or_string: true
validates :dependency_scanning, array_of_strings_or_string: true

View File

@ -35,7 +35,7 @@ module Gitlab
end
def init_metric(type, name, opts = {}, &block)
options = MetricOptions.new(opts)
options = ::Gitlab::Metrics::Methods::MetricOptions.new(opts)
options.evaluate(&block)
if disabled_by_feature(options)

View File

@ -2,6 +2,7 @@
# based on: http://stackoverflow.com/a/4883967
/home/git/gitlab/log/*.log {
su git git
daily
missingok
rotate 90
@ -11,6 +12,7 @@
}
/home/git/gitlab-shell/gitlab-shell.log {
su git git
daily
missingok
rotate 90

View File

@ -999,9 +999,6 @@ msgstr ""
msgid "A Let's Encrypt account will be configured for this GitLab installation using your email address. You will receive emails to warn of expiring certificates."
msgstr ""
msgid "A Terraform report was generated in your pipelines."
msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
@ -3823,6 +3820,9 @@ msgstr ""
msgid "By %{user_name}"
msgstr ""
msgid "By URL"
msgstr ""
msgid "By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format."
msgstr ""
@ -6552,6 +6552,12 @@ msgstr ""
msgid "Coverage"
msgstr ""
msgid "Coverage Fuzzing"
msgstr ""
msgid "Crash State"
msgstr ""
msgid "Create"
msgstr ""
@ -10028,6 +10034,9 @@ msgstr ""
msgid "Find File"
msgstr ""
msgid "Find bugs in your code with coverage-guided fuzzing"
msgstr ""
msgid "Find by path"
msgstr ""
@ -10274,9 +10283,6 @@ msgstr ""
msgid "Generate new export"
msgstr ""
msgid "Generating the report caused an error."
msgstr ""
msgid "Geo"
msgstr ""
@ -13862,6 +13868,9 @@ msgstr ""
msgid "Maximum field length"
msgstr ""
msgid "Maximum file size is 2MB. Please select a smaller file."
msgstr ""
msgid "Maximum import size (MB)"
msgstr ""
@ -16660,6 +16669,9 @@ msgstr ""
msgid "Please check your email (%{email}) to verify that you own this address and unlock the power of CI/CD. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}."
msgstr ""
msgid "Please choose a file"
msgstr ""
msgid "Please choose a group URL with no special characters."
msgstr ""
@ -17785,12 +17797,6 @@ msgstr ""
msgid "ProjectSettings|Repository"
msgstr ""
msgid "ProjectSettings|Requirements"
msgstr ""
msgid "ProjectSettings|Requirements management system for this project"
msgstr ""
msgid "ProjectSettings|Share code pastes with others out of Git repository"
msgstr ""
@ -19100,9 +19106,6 @@ msgstr ""
msgid "Reported %{timeAgo} by %{reportedBy}"
msgstr ""
msgid "Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete"
msgstr ""
msgid "Reporter"
msgstr ""
@ -20310,6 +20313,9 @@ msgstr ""
msgid "Select due date"
msgstr ""
msgid "Select file"
msgstr ""
msgid "Select group or project"
msgstr ""
@ -21556,6 +21562,9 @@ msgstr ""
msgid "Stack trace"
msgstr ""
msgid "Stacktrace snippet"
msgstr ""
msgid "Stage"
msgstr ""
@ -22363,6 +22372,34 @@ msgstr ""
msgid "Terms of Service and Privacy Policy"
msgstr ""
msgid "Terraform|%{number} Terraform report failed to generate"
msgid_plural "Terraform|%{number} Terraform reports failed to generate"
msgstr[0] ""
msgstr[1] ""
msgid "Terraform|%{number} Terraform report was generated in your pipelines"
msgid_plural "Terraform|%{number} Terraform reports were generated in your pipelines"
msgstr[0] ""
msgstr[1] ""
msgid "Terraform|A Terraform report failed to generate."
msgstr ""
msgid "Terraform|A Terraform report was generated in your pipelines."
msgstr ""
msgid "Terraform|Generating the report caused an error."
msgstr ""
msgid "Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete"
msgstr ""
msgid "Terraform|The Terraform report %{name} failed to generate."
msgstr ""
msgid "Terraform|The Terraform report %{name} was generated in your pipelines."
msgstr ""
msgid "Test"
msgstr ""
@ -22490,9 +22527,6 @@ msgstr ""
msgid "The Prometheus server responded with \"bad request\". Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}"
msgstr ""
msgid "The Terraform report %{name} was generated in your pipelines."
msgstr ""
msgid "The URL defined on the primary node that secondary nodes should use to contact it."
msgstr ""
@ -25575,6 +25609,9 @@ msgstr ""
msgid "Vulnerability|Class"
msgstr ""
msgid "Vulnerability|Crash Address"
msgstr ""
msgid "Vulnerability|Description"
msgstr ""
@ -26866,6 +26903,9 @@ msgstr ""
msgid "ciReport|Container scanning detects known vulnerabilities in your docker images."
msgstr ""
msgid "ciReport|Coverage Fuzzing"
msgstr ""
msgid "ciReport|Create a merge request to implement this solution, or download and apply the patch manually."
msgstr ""
@ -27177,9 +27217,6 @@ msgstr ""
msgid "https://your-bitbucket-server"
msgstr ""
msgid "image"
msgstr ""
msgid "image diff"
msgstr ""

View File

@ -309,6 +309,7 @@ module QA
autoload :New, 'qa/page/project/issue/new'
autoload :Show, 'qa/page/project/issue/show'
autoload :Index, 'qa/page/project/issue/index'
autoload :JiraImport, 'qa/page/project/issue/jira_import'
end
module Fork

View File

@ -18,6 +18,11 @@ module QA
element :export_issues_modal
end
view 'app/views/projects/issues/import_csv/_button.html.haml' do
element :import_issues_button
element :import_from_jira_link
end
view 'app/views/projects/issues/_issue.html.haml' do
element :issue
element :issue_link, 'link_to issue.title' # rubocop:disable QA/ElementWithPattern
@ -51,10 +56,25 @@ module QA
click_element(:export_issues_button)
end
def click_import_from_jira_link
click_element(:import_from_jira_link)
end
def click_import_issues_dropdown
# When there are no issues, the image that loads causes the buttons to jump
has_loaded_all_images?
click_element(:import_issues_button)
end
def export_issues_modal
find_element(:export_issues_modal)
end
def go_to_jira_import_form
click_import_issues_dropdown
click_import_from_jira_link
end
def has_assignee_link_count?(count)
all_elements(:assignee_link, count: count)
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module QA
module Page
module Project
module Issue
class JiraImport < Page::Base
view 'app/assets/javascripts/jira_import/components/jira_import_form.vue' do
element :jira_project_dropdown
element :jira_issues_import_button
end
def select_jira_project(jira_project)
select_element(:jira_project_dropdown, jira_project)
end
def select_project_and_import(jira_project)
select_jira_project(jira_project)
click_element(:jira_issues_import_button)
end
end
end
end
end
end

View File

@ -0,0 +1,75 @@
# frozen_string_literal: true
module QA
context 'Plan' do
describe 'Jira issue import', :jira, :orchestrated, :requires_admin do
let(:jira_project_key) { "JITD" }
let(:jira_issue_title) { "[#{jira_project_key}-1] Jira to GitLab Test Issue" }
let(:jira_issue_description) { "This issue is for testing importing Jira issues to GitLab." }
let(:jira_issue_label_1) { "jira-import::#{jira_project_key}-1" }
let(:jira_issue_label_2) { "QA" }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = "jira_issue_import"
end
end
it 'imports issues from Jira' do
set_up_jira_integration
import_jira_issues
QA::Support::Retrier.retry_on_exception do
Page::Project::Menu.perform(&:click_issues)
Page::Project::Issue::Index.perform do |issues_page|
issues_page.click_issue_link(jira_issue_title)
end
end
expect(page).to have_content(jira_issue_description)
Page::Project::Issue::Show.perform do |issue|
expect(issue).to have_label(jira_issue_label_1)
expect(issue).to have_label(jira_issue_label_2)
end
end
private
def set_up_jira_integration
# Retry is required because allow_local_requests_from_web_hooks_and_services
# takes some time to get enabled.
# Bug issue: https://gitlab.com/gitlab-org/gitlab/-/issues/217010
QA::Support::Retrier.retry_on_exception(max_attempts: 5, sleep_interval: 3) do
Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true)
page.visit Runtime::Scenario.gitlab_address
Flow::Login.sign_in_unless_signed_in
project.visit!
Page::Project::Menu.perform(&:go_to_integrations_settings)
QA::Page::Project::Settings::Integrations.perform(&:click_jira_link)
QA::Page::Project::Settings::Services::Jira.perform do |jira|
jira.setup_service_with(url: Vendor::Jira::JiraAPI.perform(&:base_url))
end
expect(page).not_to have_text("Url is blocked")
expect(page).to have_text("Jira activated")
end
end
def import_jira_issues
Page::Project::Menu.perform(&:click_issues)
Page::Project::Issue::Index.perform(&:go_to_jira_import_form)
Page::Project::Issue::JiraImport.perform do |form|
form.select_project_and_import(jira_project_key)
end
expect(page).to have_content("Import in progress")
end
end
end
end

View File

@ -40,7 +40,7 @@ class AutomatedCleanup
end
def review_apps_namespace
self.class.ee? ? 'review-apps-ee' : 'review-apps-ce'
'review-apps'
end
def helm

View File

@ -7,7 +7,7 @@ global:
external-dns.alpha.kubernetes.io/ttl: 10
configureCertmanager: false
tls:
secretName: tls-cert
secretName: review-apps-tls
initialRootPassword:
secret: shared-gitlab-initial-root-password
certmanager:
@ -61,11 +61,11 @@ gitlab:
task-runner:
resources:
requests:
cpu: 50m
memory: 350M
cpu: 300m
memory: 800M
limits:
cpu: 100m
memory: 700M
cpu: 450m
memory: 1200M
webservice:
resources:
requests:

View File

@ -11,7 +11,7 @@ function setup_gcp_dependencies() {
# These scripts require the following environment variables:
# - REVIEW_APPS_GCP_REGION - e.g `us-central1`
# - KUBE_NAMESPACE - e.g `review-apps-ee`
# - KUBE_NAMESPACE - e.g `review-apps`
function delete_firewall_rules() {
if [[ ${#@} -eq 0 ]]; then

View File

@ -66,7 +66,7 @@ function kubectl_cleanup_release() {
local release="${2}"
echoinfo "Deleting all K8s resources matching '${release}'..." true
kubectl --namespace "${namespace}" get ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa,crd 2>&1 \
kubectl --namespace "${namespace}" get ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,clusterrole,clusterrolebinding,role,rolebinding,sa,crd 2>&1 \
| grep "${release}" \
| awk '{print $1}' \
| xargs kubectl --namespace "${namespace}" delete \
@ -126,6 +126,38 @@ function get_pod() {
echo "${pod_name}"
}
function run_task() {
local namespace="${KUBE_NAMESPACE}"
local ruby_cmd="${1}"
local task_runner_pod=$(get_pod "task-runner")
kubectl exec -it --namespace "${namespace}" "${task_runner_pod}" -- gitlab-rails runner "${ruby_cmd}"
}
function disable_sign_ups() {
if [ -z ${REVIEW_APPS_ROOT_TOKEN+x} ]; then
echoerr "In order to protect Review Apps, REVIEW_APPS_ROOT_TOKEN variable must be set"
false
else
true
fi
# Create the root token
local ruby_cmd="token = User.find_by_username('root').personal_access_tokens.create(scopes: [:api], name: 'Token to disable sign-ups'); token.set_token('${REVIEW_APPS_ROOT_TOKEN}'); begin; token.save!; rescue(ActiveRecord::RecordNotUnique); end"
run_task "${ruby_cmd}"
# Disable sign-ups
curl --silent --show-error --request PUT --header "PRIVATE-TOKEN: ${REVIEW_APPS_ROOT_TOKEN}" "${CI_ENVIRONMENT_URL}/api/v4/application/settings?signup_enabled=false"
local signup_enabled=$(curl --silent --show-error --request GET --header "PRIVATE-TOKEN: ${REVIEW_APPS_ROOT_TOKEN}" "${CI_ENVIRONMENT_URL}/api/v4/application/settings" | jq ".signup_enabled")
if [[ "${signup_enabled}" == "false" ]]; then
echoinfo "Sign-ups have been disabled successfully."
else
echoerr "Sign-ups should be disabled but are still enabled!"
false
fi
}
function check_kube_domain() {
echoinfo "Checking that Kube domain exists..." true
@ -181,6 +213,32 @@ function install_external_dns() {
fi
}
# This script is used to install cert-manager in the cluster
# The installation steps are documented in
# https://gitlab.com/gitlab-org/quality/team-tasks/snippets/1990286
function install_certmanager() {
local namespace="${KUBE_NAMESPACE}"
local release="cert-manager-review-app-helm3"
echoinfo "Installing cert-manager..." true
if ! deploy_exists "${namespace}" "${release}" || previous_deploy_failed "${namespace}" "${release}" ; then
kubectl apply \
-f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.10/deploy/manifests/00-crds.yaml
echoinfo "Installing cert-manager Helm chart"
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install "${release}" jetstack/cert-manager \
--namespace "${namespace}" \
--version v0.15.1 \
--set installCRDS=true
else
echoinfo "The cert-manager Helm chart is already successfully deployed."
fi
}
function create_application_secret() {
local namespace="${KUBE_NAMESPACE}"
local release="${CI_ENVIRONMENT_SLUG}"

View File

@ -27,10 +27,6 @@ RSpec.describe 'viewing issues with design references' do
MD
end
before do
stub_feature_flags(design_management_moved: false)
end
def visit_page_with_design_references
public_issue = create(:issue, project: public_project, description: description)
visit project_issue_path(public_issue.project, public_issue)

View File

@ -8,34 +8,57 @@ RSpec.describe 'User paginates issue designs', :js do
let(:project) { create(:project_empty_repo, :public) }
let(:issue) { create(:issue, project: project) }
before do
enable_design_management
stub_feature_flags(design_management_moved: false)
context 'design_management_moved flag disabled' do
before do
stub_feature_flags(design_management_moved: false)
enable_design_management
create_list(:design, 2, :with_file, issue: issue)
visit project_issue_path(project, issue)
click_link 'Designs'
wait_for_requests
find('.js-design-list-item', match: :first).click
end
it 'paginates to next design' do
expect(find('.js-previous-design')[:disabled]).to eq('true')
page.within(find('.js-design-header')) do
expect(page).to have_content('1 of 2')
create_list(:design, 2, :with_file, issue: issue)
visit project_issue_path(project, issue)
click_link 'Designs'
wait_for_requests
find('.js-design-list-item', match: :first).click
end
find('.js-next-design').click
it 'paginates to next design' do
expect(find('.js-previous-design')[:disabled]).to eq('true')
expect(find('.js-previous-design')[:disabled]).not_to eq('true')
page.within(find('.js-design-header')) do
expect(page).to have_content('1 of 2')
end
page.within(find('.js-design-header')) do
expect(page).to have_content('2 of 2')
find('.js-next-design').click
expect(find('.js-previous-design')[:disabled]).not_to eq('true')
page.within(find('.js-design-header')) do
expect(page).to have_content('2 of 2')
end
end
end
context 'design_management_moved flag enabled' do
before do
enable_design_management
create_list(:design, 2, :with_file, issue: issue)
visit project_issue_path(project, issue)
find('.js-design-list-item', match: :first).click
end
it 'paginates to next design' do
expect(find('.js-previous-design')[:disabled]).to eq('true')
page.within(find('.js-design-header')) do
expect(page).to have_content('1 of 2')
end
find('.js-next-design').click
expect(find('.js-previous-design')[:disabled]).not_to eq('true')
page.within(find('.js-design-header')) do
expect(page).to have_content('2 of 2')
end
end
end
end

View File

@ -8,18 +8,32 @@ RSpec.describe 'User design permissions', :js do
let(:project) { create(:project_empty_repo, :public) }
let(:issue) { create(:issue, project: project) }
before do
enable_design_management
stub_feature_flags(design_management_moved: false)
context 'design_management_moved flag disabled' do
before do
enable_design_management
stub_feature_flags(design_management_moved: false)
visit project_issue_path(project, issue)
visit project_issue_path(project, issue)
click_link 'Designs'
click_link 'Designs'
wait_for_requests
wait_for_requests
end
it 'user does not have permissions to upload design' do
expect(page).not_to have_field('design_file')
end
end
it 'user does not have permissions to upload design' do
expect(page).not_to have_field('design_file')
context 'design_management_moved flag enabled' do
before do
enable_design_management
visit project_issue_path(project, issue)
end
it 'user does not have permissions to upload design' do
expect(page).not_to have_field('design_file')
end
end
end

View File

@ -13,44 +13,81 @@ RSpec.describe 'User uploads new design', :js do
sign_in(user)
end
context "when the feature is available" do
before do
enable_design_management
stub_feature_flags(design_management_moved: false)
context 'design_management_moved flag disabled' do
context "when the feature is available" do
before do
enable_design_management
stub_feature_flags(design_management_moved: false)
visit project_issue_path(project, issue)
visit project_issue_path(project, issue)
click_link 'Designs'
click_link 'Designs'
wait_for_requests
end
it 'uploads designs' do
attach_file(:design_file, logo_fixture, make_visible: true)
expect(page).to have_selector('.js-design-list-item', count: 1)
within first('#designs-tab .js-design-list-item') do
expect(page).to have_content('dk.png')
wait_for_requests
end
attach_file(:design_file, gif_fixture, make_visible: true)
it 'uploads designs' do
attach_file(:design_file, logo_fixture, make_visible: true)
expect(page).to have_selector('.js-design-list-item', count: 2)
expect(page).to have_selector('.js-design-list-item', count: 1)
within first('#designs-tab .js-design-list-item') do
expect(page).to have_content('dk.png')
end
attach_file(:design_file, gif_fixture, make_visible: true)
expect(page).to have_selector('.js-design-list-item', count: 2)
end
end
context 'when the feature is not available' do
before do
stub_feature_flags(design_management_moved: false)
visit project_issue_path(project, issue)
click_link 'Designs'
wait_for_requests
end
it 'shows the message about requirements' do
expect(page).to have_content("To enable design management, you'll need to meet the requirements.")
end
end
end
context 'when the feature is not available' do
before do
visit project_issue_path(project, issue)
context 'design_management_moved flag enabled' do
context "when the feature is available" do
before do
enable_design_management
click_link 'Designs'
visit project_issue_path(project, issue)
end
wait_for_requests
it 'uploads designs' do
attach_file(:design_file, logo_fixture, make_visible: true)
expect(page).to have_selector('.js-design-list-item', count: 1)
within first('[data-testid="designs-root"] .js-design-list-item') do
expect(page).to have_content('dk.png')
end
attach_file(:design_file, gif_fixture, make_visible: true)
expect(page).to have_selector('.js-design-list-item', count: 2)
end
end
it 'shows the message about requirements' do
expect(page).to have_content("To enable design management, you'll need to meet the requirements.")
context 'when the feature is not available' do
before do
visit project_issue_path(project, issue)
end
it 'shows the message about requirements' do
expect(page).to have_content("To enable design management, you'll need to meet the requirements.")
end
end
end

View File

@ -13,7 +13,6 @@ RSpec.describe 'Users views raw design image files' do
before do
enable_design_management
stub_feature_flags(design_management_moved: false)
end
it 'serves the latest design version when no ref is given' do

View File

@ -9,22 +9,42 @@ RSpec.describe 'User views issue designs', :js do
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:design) { create(:design, :with_file, issue: issue) }
before do
enable_design_management
stub_feature_flags(design_management_moved: false)
context 'design_management_moved flag disabled' do
before do
enable_design_management
stub_feature_flags(design_management_moved: false)
visit project_issue_path(project, issue)
visit project_issue_path(project, issue)
click_link 'Designs'
end
it 'opens design detail' do
click_link design.filename
page.within(find('.js-design-header')) do
expect(page).to have_content(design.filename)
click_link 'Designs'
end
expect(page).to have_selector('.js-design-image')
it 'opens design detail' do
click_link design.filename
page.within(find('.js-design-header')) do
expect(page).to have_content(design.filename)
end
expect(page).to have_selector('.js-design-image')
end
end
context 'design_management_moved flag enabled' do
before do
enable_design_management
visit project_issue_path(project, issue)
end
it 'opens design detail' do
click_link design.filename
page.within(find('.js-design-header')) do
expect(page).to have_content(design.filename)
end
expect(page).to have_selector('.js-design-image')
end
end
end

View File

@ -9,40 +9,78 @@ RSpec.describe 'User views issue designs', :js do
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:design) { create(:design, :with_file, issue: issue) }
before do
enable_design_management
stub_feature_flags(design_management_moved: false)
end
context 'navigates from the issue view' do
context 'design_management_moved flag disabled' do
before do
visit project_issue_path(project, issue)
click_link 'Designs'
wait_for_requests
enable_design_management
stub_feature_flags(design_management_moved: false)
end
it 'fetches list of designs' do
expect(page).to have_selector('.js-design-list-item', count: 1)
context 'navigates from the issue view' do
before do
visit project_issue_path(project, issue)
click_link 'Designs'
wait_for_requests
end
it 'fetches list of designs' do
expect(page).to have_selector('.js-design-list-item', count: 1)
end
end
context 'navigates directly to the design collection view' do
before do
visit designs_project_issue_path(project, issue)
end
it 'expands the sidebar' do
expect(page).to have_selector('.layout-page.right-sidebar-expanded')
end
end
context 'navigates directly to the individual design view' do
before do
visit designs_project_issue_path(project, issue, vueroute: design.filename)
end
it 'sees the design' do
expect(page).to have_selector('.js-design-detail')
end
end
end
context 'navigates directly to the design collection view' do
context 'design_management_moved flag enabled' do
before do
visit designs_project_issue_path(project, issue)
enable_design_management
end
it 'expands the sidebar' do
expect(page).to have_selector('.layout-page.right-sidebar-expanded')
end
end
context 'navigates from the issue view' do
before do
visit project_issue_path(project, issue)
end
context 'navigates directly to the individual design view' do
before do
visit designs_project_issue_path(project, issue, vueroute: design.filename)
it 'fetches list of designs' do
expect(page).to have_selector('.js-design-list-item', count: 1)
end
end
it 'sees the design' do
expect(page).to have_selector('.js-design-detail')
context 'navigates directly to the design collection view' do
before do
visit designs_project_issue_path(project, issue)
end
it 'expands the sidebar' do
expect(page).to have_selector('.layout-page.right-sidebar-expanded')
end
end
context 'navigates directly to the individual design view' do
before do
visit designs_project_issue_path(project, issue, vueroute: design.filename)
end
it 'sees the design' do
expect(page).to have_selector('.js-design-detail')
end
end
end
end

View File

@ -12,7 +12,6 @@ RSpec.describe 'User views an SVG design that contains XSS', :js do
before do
enable_design_management
stub_feature_flags(design_management_moved: false)
visit designs_project_issue_path(
project,
@ -30,6 +29,7 @@ RSpec.describe 'User views an SVG design that contains XSS', :js do
end
it 'displays the SVG' do
find("[data-testid='close-design']").click
expect(page).to have_selector("img.design-img[alt='xss.svg']", count: 1, visible: false)
end

View File

@ -3,7 +3,7 @@ import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import AlertDetails from '~/alert_management/components/alert_details.vue';
import createIssueQuery from '~/alert_management/graphql/mutations/create_issue_from_alert.graphql';
import createIssueMutation from '~/alert_management/graphql/mutations/create_issue_from_alert.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
import {
trackAlertsDetailsViewsOptions,
@ -25,14 +25,14 @@ describe('AlertDetails', () => {
function mountComponent({ data, loading = false, mountMethod = shallowMount, stubs = {} } = {}) {
wrapper = mountMethod(AlertDetails, {
propsData: {
provide: {
alertId: 'alertId',
projectPath,
projectIssuesPath,
projectId,
},
data() {
return { alert: { ...mockAlert }, ...data };
return { alert: { ...mockAlert }, sidebarStatus: false, ...data };
},
mocks: {
$apollo: {
@ -41,6 +41,7 @@ describe('AlertDetails', () => {
alert: {
loading,
},
sidebarStatus: {},
},
},
},
@ -135,7 +136,7 @@ describe('AlertDetails', () => {
it('should display "View issue" button that links the issue page when issue exists', () => {
const issueIid = '3';
mountComponent({
data: { alert: { ...mockAlert, issueIid } },
data: { alert: { ...mockAlert, issueIid }, sidebarStatus: false },
});
expect(findViewIssueBtn().exists()).toBe(true);
expect(findViewIssueBtn().attributes('href')).toBe(joinPaths(projectIssuesPath, issueIid));
@ -148,8 +149,11 @@ describe('AlertDetails', () => {
mountMethod: mount,
data: { alert: { ...mockAlert, issueIid } },
});
expect(findViewIssueBtn().exists()).toBe(false);
expect(findCreateIssueBtn().exists()).toBe(true);
return wrapper.vm.$nextTick().then(() => {
expect(findViewIssueBtn().exists()).toBe(false);
expect(findCreateIssueBtn().exists()).toBe(true);
});
});
it('calls `$apollo.mutate` with `createIssueQuery`', () => {
@ -160,7 +164,7 @@ describe('AlertDetails', () => {
findCreateIssueBtn().trigger('click');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: createIssueQuery,
mutation: createIssueMutation,
variables: {
iid: mockAlert.iid,
projectPath,

View File

@ -11,20 +11,28 @@ describe('Alert Details Sidebar', () => {
let wrapper;
let mock;
function mountComponent({
sidebarCollapsed = true,
mountMethod = shallowMount,
stubs = {},
alert = {},
} = {}) {
function mountComponent({ mountMethod = shallowMount, stubs = {}, alert = {} } = {}) {
wrapper = mountMethod(AlertSidebar, {
data() {
return {
sidebarStatus: false,
};
},
propsData: {
alert,
sidebarCollapsed,
},
provide: {
projectPath: 'projectPath',
projectId: '1',
},
stubs,
mocks: {
$apollo: {
queries: {
sidebarStatus: {},
},
},
},
});
}
@ -42,7 +50,7 @@ describe('Alert Details Sidebar', () => {
});
it('open as default', () => {
expect(wrapper.props('sidebarCollapsed')).toBe(true);
expect(wrapper.classes('right-sidebar-expanded')).toBe(true);
});
it('should render side bar assignee dropdown', () => {

View File

@ -61,6 +61,10 @@ describe('Design discussions component', () => {
...data,
};
},
provide: {
projectPath: 'project-path',
issueIid: '1',
},
mocks: {
$apollo,
$route: {

View File

@ -7,6 +7,7 @@ exports[`Design management toolbar component renders design and updated data 1`]
<a
aria-label="Go back to designs"
class="mr-3 text-plain d-flex justify-content-center align-items-center"
data-testid="close-design"
>
<icon-stub
name="close"

View File

@ -1,7 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management index page designs does not render toolbar when there is no permission 1`] = `
<div>
<div
data-testid="designs-root"
>
<!---->
<div
@ -73,7 +75,9 @@ exports[`Design management index page designs does not render toolbar when there
`;
exports[`Design management index page designs renders designs list and header with upload button 1`] = `
<div>
<div
data-testid="designs-root"
>
<header
class="row-content-block border-top-0 p-2 d-flex"
>
@ -188,7 +192,9 @@ exports[`Design management index page designs renders designs list and header wi
`;
exports[`Design management index page designs renders error 1`] = `
<div>
<div
data-testid="designs-root"
>
<!---->
<div
@ -216,7 +222,9 @@ exports[`Design management index page designs renders error 1`] = `
`;
exports[`Design management index page designs renders loading icon 1`] = `
<div>
<div
data-testid="designs-root"
>
<!---->
<div
@ -236,7 +244,9 @@ exports[`Design management index page designs renders loading icon 1`] = `
`;
exports[`Design management index page when has no designs renders empty text 1`] = `
<div>
<div
data-testid="designs-root"
>
<!---->
<div

View File

@ -10,7 +10,7 @@ exports[`Design management design index page renders design index 1`] = `
<design-destroyer-stub
filenames="test.jpg"
iid="1"
projectpath=""
project-path="project-path"
/>
<!---->
@ -60,7 +60,7 @@ exports[`Design management design index page renders design index 1`] = `
designid="test"
discussion="[object Object]"
discussionwithopenform=""
markdownpreviewpath="//preview_markdown?target_type=Issue"
markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
noteableid="design-id"
/>
@ -108,7 +108,7 @@ exports[`Design management design index page renders design index 1`] = `
designid="test"
discussion="[object Object]"
discussionwithopenform=""
markdownpreviewpath="//preview_markdown?target_type=Issue"
markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
noteableid="design-id"
/>
</gl-collapse-stub>
@ -140,7 +140,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
<design-destroyer-stub
filenames="test.jpg"
iid="1"
projectpath=""
project-path="project-path"
/>
<div

View File

@ -95,9 +95,12 @@ describe('Design management design index page', () => {
DesignSidebar,
DesignReplyForm,
},
provide: {
issueIid: '1',
projectPath: 'project-path',
},
data() {
return {
issueIid: '1',
activeDiscussion: {
id: null,
source: null,
@ -149,7 +152,7 @@ describe('Design management design index page', () => {
expect(findSidebar().props()).toEqual({
design,
markdownPreviewPath: '//preview_markdown?target_type=Issue',
markdownPreviewPath: '/project-path/preview_markdown?target_type=Issue',
resolvedDiscussionsExpanded: false,
});
});

View File

@ -92,19 +92,23 @@ describe('Design management index page', () => {
};
wrapper = shallowMount(Index, {
data() {
return {
designs,
allVersions,
permissions: {
createDesign,
},
};
},
mocks: { $apollo },
localVue,
router,
stubs: { DesignDestroyer, ApolloMutation, ...stubs },
attachToDocument: true,
});
wrapper.setData({
designs,
allVersions,
issueIid: '1',
permissions: {
createDesign,
provide: {
projectPath: 'project-path',
issueIid: '1',
},
});
}
@ -117,9 +121,7 @@ describe('Design management index page', () => {
it('renders loading icon', () => {
createComponent({ loading: true });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
});
expect(wrapper.element).toMatchSnapshot();
});
it('renders error', () => {
@ -135,25 +137,19 @@ describe('Design management index page', () => {
it('renders a toolbar with buttons when there are designs', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
return wrapper.vm.$nextTick().then(() => {
expect(findToolbar().exists()).toBe(true);
});
expect(findToolbar().exists()).toBe(true);
});
it('renders designs list and header with upload button', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
});
expect(wrapper.element).toMatchSnapshot();
});
it('does not render toolbar when there is no permission', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
});
expect(wrapper.element).toMatchSnapshot();
});
});
@ -185,7 +181,7 @@ describe('Design management index page', () => {
mutation: uploadDesignQuery,
variables: {
files: [{ name: 'test' }],
projectPath: '',
projectPath: 'project-path',
iid: '1',
},
optimisticResponse: {
@ -442,9 +438,9 @@ describe('Design management index page', () => {
});
});
it('on latest version when has no designs does not render toolbar buttons', () => {
it('on latest version when has no designs toolbar buttons are invisible', () => {
createComponent({ designs: [], allVersions: [mockVersion] });
expect(findToolbar().exists()).toBe(false);
expect(findToolbar().classes()).toContain('d-none');
});
describe('on non-latest version', () => {
@ -535,7 +531,7 @@ describe('Design management index page', () => {
it('ensures fullscreen layout is not applied', () => {
createComponent(true);
wrapper.vm.$router.push('/designs');
wrapper.vm.$router.push('/');
expect(mockPageEl.classList.remove).toHaveBeenCalledTimes(1);
expect(mockPageEl.classList.remove).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
});

Some files were not shown because too many files have changed in this diff Show More