Commit 17e8e8d3 authored by John T Skarbek's avatar John T Skarbek

Merge remote-tracking branch 'origin/master'

parents 076d199d 41fed29a
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.21-chrome-71.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.21-chrome-73.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29"
variables: variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1" MYSQL_ALLOW_EMPTY_PASSWORD: "1"
...@@ -28,6 +28,8 @@ stages: ...@@ -28,6 +28,8 @@ stages:
- prepare - prepare
- merge - merge
- test - test
- review
- qa
- post-test - post-test
- pages - pages
- post-cleanup - post-cleanup
......
...@@ -9,7 +9,7 @@ cloud-native-image: ...@@ -9,7 +9,7 @@ cloud-native-image:
cache: {} cache: {}
when: manual when: manual
script: script:
- gem install gitlab --no-document - install_gitlab_gem
- CNG_PROJECT_PATH="gitlab-org/build/CNG" BUILD_TRIGGER_TOKEN=$CI_JOB_TOKEN ./scripts/trigger-build cng - CNG_PROJECT_PATH="gitlab-org/build/CNG" BUILD_TRIGGER_TOKEN=$CI_JOB_TOKEN ./scripts/trigger-build cng
only: only:
- tags@gitlab-org/gitlab-ce - tags@gitlab-org/gitlab-ce
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
gitlab:assets:compile: gitlab:assets:compile:
<<: *assets-compile-cache <<: *assets-compile-cache
extends: .dedicated-no-docs-pull-cache-job extends: .dedicated-no-docs-pull-cache-job
image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.21-chrome-71.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1 image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.21-chrome-73.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1
dependencies: dependencies:
- setup-test-env - setup-test-env
services: services:
...@@ -38,6 +38,10 @@ gitlab:assets:compile: ...@@ -38,6 +38,10 @@ gitlab:assets:compile:
- bundle exec rake gitlab:assets:compile - bundle exec rake gitlab:assets:compile
- time scripts/build_assets_image - time scripts/build_assets_image
- scripts/clean-old-cached-assets - scripts/clean-old-cached-assets
# Play dependent manual jobs
- install_api_client_dependencies_with_apt
- play_job "review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
- play_job "schedule:review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
artifacts: artifacts:
name: webpack-report name: webpack-report
expire_in: 31d expire_in: 31d
...@@ -103,7 +107,7 @@ gitlab:ui:visual: ...@@ -103,7 +107,7 @@ gitlab:ui:visual:
- $CI_COMMIT_MESSAGE =~ /\[skip visual\]/i - $CI_COMMIT_MESSAGE =~ /\[skip visual\]/i
artifacts: artifacts:
paths: paths:
- tests/__image_snapshots__/ - gitlab-ui/tests/__image_snapshots__/
when: always when: always
karma: karma:
......
package-and-qa: package-and-qa:
image: ruby:2.5-alpine image: ruby:2.5-alpine
stage: test stage: qa
when: manual
before_script: [] before_script: []
dependencies: [] dependencies: []
cache: {} cache: {}
variables: variables:
GIT_DEPTH: "1" GIT_DEPTH: "1"
API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
retry: 0 retry: 0
script: script:
- apk add --update openssl curl jq - source scripts/utils.sh
- gem install gitlab --no-document - install_gitlab_gem
- source ./scripts/review_apps/review-apps.sh
- wait_for_job_to_be_done "gitlab:assets:compile"
- ./scripts/trigger-build omnibus - ./scripts/trigger-build omnibus
when: manual
only: only:
- /.+/@gitlab-org/gitlab-ce - /.+/@gitlab-org/gitlab-ce
- /.+/@gitlab-org/gitlab-ee - /.+/@gitlab-org/gitlab-ee
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
.rspec-metadata-pg-10: &rspec-metadata-pg-10 .rspec-metadata-pg-10: &rspec-metadata-pg-10
<<: *rspec-metadata <<: *rspec-metadata
<<: *use-pg-10 <<: *use-pg-10
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.21-chrome-71.0-node-10.x-yarn-1.12-postgresql-10-graphicsmagick-1.3.29" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.21-chrome-73.0-node-10.x-yarn-1.12-postgresql-10-graphicsmagick-1.3.29"
.rspec-metadata-mysql: &rspec-metadata-mysql .rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata <<: *rspec-metadata
......
...@@ -26,12 +26,10 @@ ...@@ -26,12 +26,10 @@
extends: .dedicated-runner extends: .dedicated-runner
<<: *review-only <<: *review-only
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
stage: test
cache: {} cache: {}
dependencies: [] dependencies: []
environment: &review-environment before_script:
name: review/${CI_COMMIT_REF_NAME} - source scripts/utils.sh
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
.review-docker: &review-docker .review-docker: &review-docker
<<: *review-base <<: *review-base
...@@ -42,18 +40,13 @@ ...@@ -42,18 +40,13 @@
- gitlab-org - gitlab-org
- docker - docker
variables: &review-docker-variables variables: &review-docker-variables
GIT_DEPTH: "1"
DOCKER_DRIVER: overlay2 DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://docker:2375 DOCKER_HOST: tcp://docker:2375
LATEST_QA_IMAGE: "gitlab/${CI_PROJECT_NAME}-qa:nightly" LATEST_QA_IMAGE: "gitlab/${CI_PROJECT_NAME}-qa:nightly"
QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab/${CI_PROJECT_NAME}-qa:${CI_COMMIT_REF_SLUG}" QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab/${CI_PROJECT_NAME}-qa:${CI_COMMIT_REF_SLUG}"
before_script: []
build-qa-image: build-qa-image:
<<: *review-docker <<: *review-docker
variables:
<<: *review-docker-variables
GIT_DEPTH: "20"
stage: prepare stage: prepare
script: script:
- time docker build --cache-from ${LATEST_QA_IMAGE} --tag ${QA_IMAGE} ./qa/ - time docker build --cache-from ${LATEST_QA_IMAGE} --tag ${QA_IMAGE} ./qa/
...@@ -63,16 +56,14 @@ build-qa-image: ...@@ -63,16 +56,14 @@ build-qa-image:
.review-build-cng-base: &review-build-cng-base .review-build-cng-base: &review-build-cng-base
image: ruby:2.5-alpine image: ruby:2.5-alpine
stage: test stage: test
before_script: [] when: manual
before_script:
- source scripts/utils.sh
- install_api_client_dependencies_with_apk
- install_gitlab_gem
dependencies: [] dependencies: []
cache: {} cache: {}
variables:
API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
script: script:
- apk add --update openssl curl jq
- gem install gitlab --no-document
- source ./scripts/review_apps/review-apps.sh
- wait_for_job_to_be_done "gitlab:assets:compile"
- BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng - BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
review-build-cng: review-build-cng:
...@@ -85,26 +76,32 @@ schedule:review-build-cng: ...@@ -85,26 +76,32 @@ schedule:review-build-cng:
.review-deploy-base: &review-deploy-base .review-deploy-base: &review-deploy-base
<<: *review-base <<: *review-base
stage: review
retry: 2 retry: 2
allow_failure: true allow_failure: true
variables: variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}" DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "master" GITLAB_HELM_CHART_REF: "master"
API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}" environment: &review-environment
environment: name: review/${CI_COMMIT_REF_NAME}
<<: *review-environment url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
on_stop: review-stop on_stop: review-stop
before_script: before_script:
- export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION) - export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
- export GITALY_VERSION=$(<GITALY_SERVER_VERSION) - export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
- export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION) - export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
- apk update && apk add jq - echo "${CI_ENVIRONMENT_URL}" > review_app_url.txt
- gem install gitlab --no-document - source scripts/utils.sh
- source ./scripts/review_apps/review-apps.sh - install_api_client_dependencies_with_apk
- source scripts/review_apps/review-apps.sh
script: script:
- wait_for_job_to_be_done "review-build-cng"
- perform_review_app_deployment - perform_review_app_deployment
artifacts:
paths:
- review_app_url.txt
expire_in: 2 days
when: always
review-deploy: review-deploy:
<<: *review-deploy-base <<: *review-deploy-base
...@@ -113,15 +110,29 @@ schedule:review-deploy: ...@@ -113,15 +110,29 @@ schedule:review-deploy:
<<: *review-deploy-base <<: *review-deploy-base
<<: *review-schedules-only <<: *review-schedules-only
script: script:
- wait_for_job_to_be_done "schedule:review-build-cng"
- perform_review_app_deployment - perform_review_app_deployment
review-stop:
<<: *review-base
stage: review
when: manual
allow_failure: true
variables:
GIT_DEPTH: "1"
environment:
<<: *review-environment
action: stop
script:
- source scripts/review_apps/review-apps.sh
- delete
- cleanup
.review-qa-base: &review-qa-base .review-qa-base: &review-qa-base
<<: *review-docker <<: *review-docker
stage: qa
allow_failure: true allow_failure: true
variables: variables:
<<: *review-docker-variables <<: *review-docker-variables
API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa" QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa"
QA_CAN_TEST_GIT_PROTOCOL_V2: "false" QA_CAN_TEST_GIT_PROTOCOL_V2: "false"
GITLAB_USERNAME: "root" GITLAB_USERNAME: "root"
...@@ -131,40 +142,45 @@ schedule:review-deploy: ...@@ -131,40 +142,45 @@ schedule:review-deploy:
GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}" GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}" EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
QA_DEBUG: "true" QA_DEBUG: "true"
dependencies:
- review-deploy
artifacts: artifacts:
paths: paths:
- ./qa/gitlab-qa-run-* - ./qa/gitlab-qa-run-*
expire_in: 7 days expire_in: 7 days
when: always when: always
before_script: before_script:
- echo "${QA_IMAGE}" - export CI_ENVIRONMENT_URL="$(cat review_app_url.txt)"
- echo "${CI_ENVIRONMENT_URL}" - echo "${CI_ENVIRONMENT_URL}"
- apk update && apk add curl jq - echo "${QA_IMAGE}"
- source ./scripts/review_apps/review-apps.sh - source scripts/utils.sh
- install_api_client_dependencies_with_apk
- gem install gitlab-qa --no-document ${GITLAB_QA_VERSION:+ --version ${GITLAB_QA_VERSION}} - gem install gitlab-qa --no-document ${GITLAB_QA_VERSION:+ --version ${GITLAB_QA_VERSION}}
review-qa-smoke: review-qa-smoke:
<<: *review-qa-base <<: *review-qa-base
retry: 2 retry: 2
script: script:
- wait_for_job_to_be_done "review-deploy"
- gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" - gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
review-qa-all: review-qa-all:
<<: *review-qa-base <<: *review-qa-base
when: manual
script: script:
- wait_for_job_to_be_done "review-deploy"
- gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" - gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
when: manual
.review-performance-base: &review-performance-base .review-performance-base: &review-performance-base
<<: *review-qa-base <<: *review-qa-base
script: stage: qa
- wait_for_job_to_be_done "review-deploy" before_script:
- export CI_ENVIRONMENT_URL="$(cat review_app_url.txt)"
- echo "${CI_ENVIRONMENT_URL}"
- mkdir -p gitlab-exporter - mkdir -p gitlab-exporter
- wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- mkdir sitespeed-results - mkdir -p sitespeed-results
- docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" script:
- docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "${CI_ENVIRONMENT_URL}"
after_script:
- mv sitespeed-results/data/performance.json performance.json - mv sitespeed-results/data/performance.json performance.json
artifacts: artifacts:
paths: paths:
...@@ -175,42 +191,26 @@ review-qa-all: ...@@ -175,42 +191,26 @@ review-qa-all:
review-performance: review-performance:
<<: *review-performance-base <<: *review-performance-base
review-stop: schedule:review-performance:
<<: *review-base <<: *review-performance-base
extends: .single-script-job-dedicated-runner <<: *review-schedules-only
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base dependencies:
allow_failure: true - schedule:review-deploy
variables:
SCRIPT_NAME: "review_apps/review-apps.sh"
when: manual
environment:
<<: *review-environment
action: stop
script:
- source $(basename "${SCRIPT_NAME}")
- delete
- cleanup
schedule:review-cleanup: schedule:review-cleanup:
<<: *review-base <<: *review-base
<<: *review-schedules-only <<: *review-schedules-only
stage: build stage: build
allow_failure: true allow_failure: true
variables:
GIT_DEPTH: "1"
environment: environment:
name: review/auto-cleanup name: review/auto-cleanup
action: stop
before_script: before_script:
- gem install gitlab --no-document - source scripts/utils.sh
- install_gitlab_gem
script: script:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb - ruby -rrubygems scripts/review_apps/automated_cleanup.rb
schedule:review-performance:
<<: *review-performance-base
<<: *review-schedules-only
script:
- wait_for_job_to_be_done "schedule:review-deploy"
danger-review: danger-review:
extends: .dedicated-pull-cache-job extends: .dedicated-pull-cache-job
image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"plugins":[ "plugins":[
"./scripts/frontend/stylelint/stylelint-duplicate-selectors.js", "./scripts/frontend/stylelint/stylelint-duplicate-selectors.js",
"./scripts/frontend/stylelint/stylelint-utility-classes.js", "./scripts/frontend/stylelint/stylelint-utility-classes.js",
"stylelint-scss", "stylelint-scss"
], ],
"rules":{ "rules":{
"at-rule-blacklist":[ "at-rule-blacklist":[
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
"number-leading-zero":"always", "number-leading-zero":"always",
"number-no-trailing-zeros":true, "number-no-trailing-zeros":true,
"property-no-unknown":true, "property-no-unknown":true,
"property-no-vendor-prefix":true, "property-no-vendor-prefix": [true, { "ignoreProperties": ["user-select"] }],
"rule-empty-line-before":[ "rule-empty-line-before":[
"always-multi-line", "always-multi-line",
{ {
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
{ {
"message":"Selector should be written in lowercase with hyphens (selector-class-pattern)", "message":"Selector should be written in lowercase with hyphens (selector-class-pattern)",
"severity": "warning" "severity": "warning"
}, }
], ],
"selector-list-comma-newline-after":"always", "selector-list-comma-newline-after":"always",
"selector-max-compound-selectors":[3, { "severity": "warning" }], "selector-max-compound-selectors":[3, { "severity": "warning" }],
...@@ -104,8 +104,8 @@ ...@@ -104,8 +104,8 @@
"selector-pseudo-element-no-unknown":true, "selector-pseudo-element-no-unknown":true,
"shorthand-property-no-redundant-values":true, "shorthand-property-no-redundant-values":true,
"string-quotes":"single", "string-quotes":"single",
"value-no-vendor-prefix":[true, { ignoreValues: ["sticky"] }], "value-no-vendor-prefix":[true, { "ignoreValues": ["sticky"] }],
"stylelint-gitlab/duplicate-selectors":[true,{ "severity": "warning" }], "stylelint-gitlab/duplicate-selectors":[true,{ "severity": "warning" }],
"stylelint-gitlab/utility-classes":[true,{ "severity": "warning" }], "stylelint-gitlab/utility-classes":[true,{ "severity": "warning" }]
} }
} }
...@@ -79,6 +79,7 @@ gem 'rack-cors', '~> 1.0.0', require: 'rack/cors' ...@@ -79,6 +79,7 @@ gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# GraphQL API # GraphQL API
gem 'graphql', '~> 1.8.0' gem 'graphql', '~> 1.8.0'
gem 'graphiql-rails', '~> 1.4.10' gem 'graphiql-rails', '~> 1.4.10'
gem 'apollo_upload_server', '~> 2.0.0.beta3'
# Disable strong_params so that Mash does not respond to :permitted? # Disable strong_params so that Mash does not respond to :permitted?
gem 'hashie-forbidden_attributes' gem 'hashie-forbidden_attributes'
......
...@@ -52,6 +52,9 @@ GEM ...@@ -52,6 +52,9 @@ GEM
public_suffix (>= 2.0.2, < 4.0) public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1) aes_key_wrap (1.0.1)
akismet (2.0.0) akismet (2.0.0)
apollo_upload_server (2.0.0.beta.3)
graphql (>= 1.8)
rails (>= 4.2)
arel (8.0.0) arel (8.0.0)
asana (0.8.1) asana (0.8.1)
faraday (~> 0.9) faraday (~> 0.9)
...@@ -988,6 +991,7 @@ DEPENDENCIES ...@@ -988,6 +991,7 @@ DEPENDENCIES
acts-as-taggable-on (~> 6.0) acts-as-taggable-on (~> 6.0)
addressable (~> 2.5.2) addressable (~> 2.5.2)
akismet (~> 2.0) akismet (~> 2.0)
apollo_upload_server (~> 2.0.0.beta3)
asana (~> 0.8.1) asana (~> 0.8.1)
asciidoctor (~> 1.5.8) asciidoctor (~> 1.5.8)
asciidoctor-plantuml (= 0.0.8) asciidoctor-plantuml (= 0.0.8)
......
...@@ -168,7 +168,7 @@ export default { ...@@ -168,7 +168,7 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<div class="d-flex board-card-header"> <div class="d-flex board-card-header" dir="auto">
<h4 class="board-card-title append-bottom-0 prepend-top-0"> <h4 class="board-card-title append-bottom-0 prepend-top-0">
<icon <icon
v-if="issue.confidential" v-if="issue.confidential"
......
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import Vue from 'vue'; import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import PersistentUserCallout from '../persistent_user_callout'; import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale'; import { s__, sprintf } from '../locale';
import Flash from '../flash'; import Flash from '../flash';
import Poll from '../lib/utils/poll'; import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels'; import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub'; import eventHub from './event_hub';
import { import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX } from './constants';
APPLICATION_STATUS,
REQUEST_SUBMITTED,
REQUEST_FAILURE,
UPGRADE_REQUESTED,
UPGRADE_REQUEST_FAILURE,
INGRESS,
INGRESS_DOMAIN_SUFFIX,
} from './constants';
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store'; import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue'; import Applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons'; import setupToggleButtons from '../toggle_buttons';
Vue.use(GlToast);
/** /**
* Cluster page has 2 separate parts: * Cluster page has 2 separate parts:
* Toggle button and applications section * Toggle button and applications section
...@@ -134,7 +129,6 @@ export default class Clusters { ...@@ -134,7 +129,6 @@ export default class Clusters {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication); eventHub.$on('installApplication', this.installApplication);
eventHub.$on('upgradeApplication', data => this.upgradeApplication(data)); eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId));
eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId)); eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data)); eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data)); eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
...@@ -144,7 +138,6 @@ export default class Clusters { ...@@ -144,7 +138,6 @@ export default class Clusters {
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication); eventHub.$off('installApplication', this.installApplication);
eventHub.$off('upgradeApplication', this.upgradeApplication); eventHub.$off('upgradeApplication', this.upgradeApplication);
eventHub.$off('upgradeFailed', this.upgradeFailed);
eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess); eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
eventHub.$off('saveKnativeDomain'); eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname'); eventHub.$off('setKnativeHostname');
...@@ -258,12 +251,13 @@ export default class Clusters { ...@@ -258,12 +251,13 @@ export default class Clusters {
installApplication(data) { installApplication(data) {
const appId = data.id; const appId = data.id;
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUBMITTED);
this.store.updateAppProperty(appId, 'requestReason', null); this.store.updateAppProperty(appId, 'requestReason', null);
this.store.updateAppProperty(appId, 'statusReason', null); this.store.updateAppProperty(appId, 'statusReason', null);
this.store.installApplication(appId);
return this.service.installApplication(appId, data.params).catch(() => { return this.service.installApplication(appId, data.params).catch(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE); this.store.notifyInstallFailure(appId);
this.store.updateAppProperty( this.store.updateAppProperty(
appId, appId,
'requestReason', 'requestReason',
...@@ -274,17 +268,15 @@ export default class Clusters { ...@@ -274,17 +268,15 @@ export default class Clusters {
upgradeApplication(data) { upgradeApplication(data) {
const appId = data.id; const appId = data.id;
this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUESTED);
this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING);
this.service.installApplication(appId, data.params).catch(() => this.upgradeFailed(appId));
}
upgradeFailed(appId) { this.store.updateApplication(appId);
this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUEST_FAILURE); this.service.installApplication(appId, data.params).catch(() => {
this.store.notifyUpdateFailure(appId);
});
} }
dismissUpgradeSuccess(appId) { dismissUpgradeSuccess(appId) {
this.store.updateAppProperty(appId, 'requestStatus', null); this.store.acknowledgeSuccessfulUpdate(appId);
} }
toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) { toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) {
......
...@@ -8,12 +8,7 @@ import identicon from '../../vue_shared/components/identicon.vue'; ...@@ -8,12 +8,7 @@ import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue';
import UninstallApplicationButton from './uninstall_application_button.vue'; import UninstallApplicationButton from './uninstall_application_button.vue';
import { import { APPLICATION_STATUS } from '../constants';
APPLICATION_STATUS,
REQUEST_SUBMITTED,
REQUEST_FAILURE,
UPGRADE_REQUESTED,
} from '../constants';
export default { export default {
components: { components: {
...@@ -63,10 +58,6 @@ export default { ...@@ -63,10 +58,6 @@ export default {
type: String, type: String,
required: false, required: false,
}, },
requestStatus: {
type: String,
required: false,
},
requestReason: { requestReason: {
type: String, type: String,
required: false, required: false,
...@@ -76,6 +67,11 @@ export default { ...@@ -76,6 +67,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
installFailed: {
type: Boolean,
required: false,
default: false,
},
version: { version: {
type: String, type: String,
required: false, required: false,
...@@ -88,6 +84,21 @@ export default { ...@@ -88,6 +84,21 @@ export default {
type: Boolean, type: Boolean,
required: false, required: false,
}, },
updateSuccessful: {
type: Boolean,
required: false,
default: false,
},
updateFailed: {
type: Boolean,
required: false,
default: false,
},
updateAcknowledged: {
type: Boolean,
required: false,
default: true,
},
installApplicationRequestParams: { installApplicationRequestParams: {
type: Object, type: Object,
required: false, required: false,
...@@ -102,21 +113,12 @@ export default { ...@@ -102,21 +113,12 @@ export default {
return Object.values(APPLICATION_STATUS).includes(this.status); return Object.values(APPLICATION_STATUS).includes(this.status);
}, },
isInstalling() { isInstalling() {
return ( return this.status === APPLICATION_STATUS.INSTALLING;
this.status === APPLICATION_STATUS.SCHEDULED ||
this.status === APPLICATION_STATUS.INSTALLING ||
(this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.installed)
);
}, },
canInstall() { canInstall() {
if (this.isInstalling) {
return false;
}
return ( return (
this.status === APPLICATION_STATUS.NOT_INSTALLABLE || this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.status === APPLICATION_STATUS.INSTALLABLE || this.status === APPLICATION_STATUS.INSTALLABLE ||
this.status === APPLICATION_STATUS.ERROR ||
this.isUnknownStatus this.isUnknownStatus
); );
}, },
...@@ -137,7 +139,7 @@ export default { ...@@ -137,7 +139,7 @@ export default {
return !this.installed || !this.uninstallable; return !this.installed || !this.uninstallable;
}, },
installButtonLoading() { installButtonLoading() {
return !this.status || this.status === APPLICATION_STATUS.SCHEDULED || this.isInstalling; return !this.status || this.isInstalling;
}, },
installButtonDisabled() { installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but // Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
...@@ -168,19 +170,13 @@ export default { ...@@ -168,19 +170,13 @@ export default {
manageButtonLabel() { manageButtonLabel() {
return s__('ClusterIntegration|Manage'); return s__('ClusterIntegration|Manage');
}, },
hasError() {
return (
!this.isInstalling &&
(this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE)
);
},
generalErrorDescription() { generalErrorDescription() {
return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), { return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
title: this.title, title: this.title,
}); });
}, },
versionLabel() { versionLabel() {
if (this.upgradeFailed) { if (this.updateFailed) {
return s__('ClusterIntegration|Upgrade failed'); return s__('ClusterIntegration|Upgrade failed');
} else if (this.isUpgrading) { } else if (this.isUpgrading) {
return s__('ClusterIntegration|Upgrading'); return s__('ClusterIntegration|Upgrading');
...@@ -188,19 +184,6 @@ export default { ...@@ -188,19 +184,6 @@ export default {
return s__('ClusterIntegration|Upgraded'); return s__('ClusterIntegration|Upgraded');
}, },
upgradeRequested() {
return this.requestStatus === UPGRADE_REQUESTED;
},
upgradeSuccessful() {
return this.status === APPLICATION_STATUS.UPDATED;
},
upgradeFailed() {
if (this.isUpgrading) {
return false;
}
return this.status === APPLICATION_STATUS.UPDATE_ERRORED;
},
upgradeFailureDescription() { upgradeFailureDescription() {
return s__('ClusterIntegration|Update failed. Please check the logs and try again.'); return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
}, },
...@@ -211,11 +194,11 @@ export default { ...@@ -211,11 +194,11 @@ export default {
}, },
upgradeButtonLabel() { upgradeButtonLabel() {
let label; let label;
if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) { if (this.upgradeAvailable && !this.updateFailed && !this.isUpgrading) {
label = s__('ClusterIntegration|Upgrade'); label = s__('ClusterIntegration|Upgrade');
} else if (this.isUpgrading) { } else if (this.isUpgrading) {
label = s__('ClusterIntegration|Updating'); label = s__('ClusterIntegration|Updating');
} else if (this.upgradeFailed) { } else if (this.updateFailed) {
label = s__('ClusterIntegration|Retry update'); label = s__('ClusterIntegration|Retry update');
} }
...@@ -223,24 +206,19 @@ export default { ...@@ -223,24 +206,19 @@ export default {
}, },
isUpgrading() { isUpgrading() {
// Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend // Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
return ( return this.status === APPLICATION_STATUS.UPDATING;
this.status === APPLICATION_STATUS.UPDATING ||
(this.upgradeRequested && !this.upgradeSuccessful)
);
}, },
shouldShowUpgradeDetails() { shouldShowUpgradeDetails() {
// This method only returns true when; // This method only returns true when;
// Upgrade was successful OR Upgrade failed // Upgrade was successful OR Upgrade failed
// AND new upgrade is unavailable AND version information is present. // AND new upgrade is unavailable AND version information is present.
return ( return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version;
(this.upgradeSuccessful || this.upgradeFailed) && !this.upgradeAvailable && this.version
);
}, },
}, },
watch: { watch: {
status() { updateSuccessful() {
if (this.status === APPLICATION_STATUS.UPDATE_ERRORED) { if (this.updateSuccessful) {
eventHub.$emit('upgradeFailed', this.id); this.$toast.show(this.upgradeSuccessDescription);
} }
}, },
}, },
...@@ -257,9 +235,6 @@ export default { ...@@ -257,9 +235,6 @@ export default {
params: this.installApplicationRequestParams, params: this.installApplicationRequestParams,
}); });
}, },
dismissUpgradeSuccess() {
eventHub.$emit('dismissUpgradeSuccess', this.id);
},
}, },
}; };
</script> </script>
...@@ -297,7 +272,7 @@ export default { ...@@ -297,7 +272,7 @@ export default {
</strong> </strong>
<slot name="description"></slot> <slot name="description"></slot>
<div <div
v-if="hasError || isUnknownStatus" v-if="installFailed || isUnknownStatus"
class="cluster-application-error text-danger prepend-top-10" class="cluster-application-error text-danger prepend-top-10"
> >
<p class="js-cluster-application-general-error-message append-bottom-0"> <p class="js-cluster-application-general-error-message append-bottom-0">
...@@ -318,10 +293,10 @@ export default { ...@@ -318,10 +293,10 @@ export default {
class="form-text text-muted label p-0 js-cluster-application-upgrade-details" class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
> >
{{ versionLabel }} {{ versionLabel }}
<span v-if="upgradeSuccessful">to</span> <span v-if="updateSuccessful">to</span>
<gl-link <gl-link
v-if="upgradeSuccessful" v-if="updateSuccessful"
:href="chartRepo" :href="chartRepo"
target="_blank" target="_blank"
class="js-cluster-application-upgrade-version" class="js-cluster-application-upgrade-version"
...@@ -330,24 +305,13 @@ export default { ...@@ -330,24 +305,13 @@ export default {
</div> </div>
<div <div
v-if="upgradeFailed && !isUpgrading" v-if="updateFailed && !isUpgrading"
class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message" class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
> >
{{ upgradeFailureDescription }} {{ upgradeFailureDescription }}
</div> </div>
<div
v-if="upgradeRequested && upgradeSuccessful"
class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3"
>
{{ upgradeSuccessDescription }}
<button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess">
&times;
</button>
</div>
<loading-button <loading-button
v-if="upgradeAvailable || upgradeFailed || isUpgrading" v-if="upgradeAvailable || updateFailed || isUpgrading"
class="btn btn-primary js-cluster-application-upgrade-button mt-2" class="btn btn-primary js-cluster-application-upgrade-button mt-2"
:loading="isUpgrading" :loading="isUpgrading"
:disabled="isUpgrading" :disabled="isUpgrading"
...@@ -361,9 +325,9 @@ export default { ...@@ -361,9 +325,9 @@ export default {
role="gridcell" role="gridcell"
> >
<div v-if="showManageButton" class="btn-group table-action-buttons"> <div v-if="showManageButton" class="btn-group table-action-buttons">
<a :href="manageLink" :class="{ disabled: disabled }" class="btn">{{ <a :href="manageLink" :class="{ disabled: disabled }" class="btn">
manageButtonLabel {{ manageButtonLabel }}
}}</a> </a>
</div> </div>
<div class="btn-group table-action-buttons"> <div class="btn-group table-action-buttons">
<loading-button <loading-button
......
...@@ -7,6 +7,7 @@ export const CLUSTER_TYPE = { ...@@ -7,6 +7,7 @@ export const CLUSTER_TYPE = {
// These need to match what is returned from the server // These need to match what is returned from the server
export const APPLICATION_STATUS = { export const APPLICATION_STATUS = {
NO_STATUS: null,
NOT_INSTALLABLE: 'not_installable', NOT_INSTALLABLE: 'not_installable',
INSTALLABLE: 'installable', INSTALLABLE: 'installable',
SCHEDULED: 'scheduled', SCHEDULED: 'scheduled',
...@@ -27,17 +28,13 @@ export const APPLICATION_STATUS = { ...@@ -27,17 +28,13 @@ export const APPLICATION_STATUS = {
export const APPLICATION_INSTALLED_STATUSES = [ export const APPLICATION_INSTALLED_STATUSES = [
APPLICATION_STATUS.INSTALLED, APPLICATION_STATUS.INSTALLED,
APPLICATION_STATUS.UPDATING, APPLICATION_STATUS.UPDATING,
APPLICATION_STATUS.UPDATED,
APPLICATION_STATUS.UPDATE_ERRORED,
APPLICATION_STATUS.UNINSTALLING,
APPLICATION_STATUS.UNINSTALL_ERRORED,
]; ];
// These are only used client-side // These are only used client-side
export const REQUEST_SUBMITTED = 'request-submitted';
export const REQUEST_FAILURE = 'request-failure'; export const UPDATE_EVENT = 'update';
export const UPGRADE_REQUESTED = 'upgrade-requested'; export const INSTALL_EVENT = 'install';
export const UPGRADE_REQUEST_FAILURE = 'upgrade-request-failure';
export const INGRESS = 'ingress'; export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter'; export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative'; export const KNATIVE = 'knative';
......
import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '../constants';
const {
NO_STATUS,
SCHEDULED,
NOT_INSTALLABLE,
INSTALLABLE,
INSTALLING,
INSTALLED,
ERROR,
UPDATING,
UPDATED,
UPDATE_ERRORED,
} = APPLICATION_STATUS;
const applicationStateMachine = {
/* When the application initially loads, it will have `NO_STATUS`
* It will transition from `NO_STATUS` once the async backend call is completed
*/
[NO_STATUS]: {
on: {
[SCHEDULED]: {
target: INSTALLING,
},
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
[INSTALLABLE]: {
target: INSTALLABLE,
},
[INSTALLING]: {
target: INSTALLING,
},
[INSTALLED]: {
target: INSTALLED,
},
[ERROR]: {
target: INSTALLABLE,
effects: {
installFailed: true,
},
},
[UPDATING]: {
target: UPDATING,
},
[UPDATED]: {
target: INSTALLED,
},
[UPDATE_ERRORED]: {
target: INSTALLED,
effects: {
updateFailed: true,
},
},
},
},
[NOT_INSTALLABLE]: {
on: {
[INSTALLABLE]: {
target: INSTALLABLE,
},
},
},
[INSTALLABLE]: {
on: {
[INSTALL_EVENT]: {
target: INSTALLING,
effects: {
installFailed: false,
},
},
// This is possible in artificial environments for E2E testing
[INSTALLED]: {
target: INSTALLED,
},
},
},
[INSTALLING]: {
on: {
[INSTALLED]: {
target: INSTALLED,
},
[ERROR]: {
target: INSTALLABLE,
effects: {
installFailed: true,
},
},
},
},
[INSTALLED]: {
on: {
[UPDATE_EVENT]: {
target: UPDATING,
effects: {
updateFailed: false,
updateSuccessful: false,
},
},
},
},
[UPDATING]: {
on: {
[UPDATED]: {
target: INSTALLED,
effects: {
updateSuccessful: true,
updateAcknowledged: false,
},
},
[UPDATE_ERRORED]: {
target: INSTALLED,
effects: {
updateFailed: true,
},
},
},
},
};
/**
* Determines an application new state based on the application current state
* and an event. If the application current state cannot handle a given event,
* the current state is returned.
*
* @param {*} application
* @param {*} event
*/
const transitionApplicationState = (application, event) => {
const newState = applicationStateMachine[application.status].on[event];
return newState
? {
...application,
status: newState.target,
...newState.effects,
}
: application;
};
export default transitionApplicationState;
...@@ -7,7 +7,11 @@ import { ...@@ -7,7 +7,11 @@ import {
CERT_MANAGER, CERT_MANAGER,
RUNNER, RUNNER,
APPLICATION_INSTALLED_STATUSES, APPLICATION_INSTALLED_STATUSES,
APPLICATION_STATUS,
INSTALL_EVENT,
UPDATE_EVENT,
} from '../constants'; } from '../constants';
import transitionApplicationState from '../services/application_state_machine';
const isApplicationInstalled = appStatus => APPLICATION_INSTALLED_STATUSES.includes(appStatus); const isApplicationInstalled = appStatus => APPLICATION_INSTALLED_STATUSES.includes(appStatus);
...@@ -15,8 +19,8 @@ const applicationInitialState = { ...@@ -15,8 +19,8 @@ const applicationInitialState = {
status: null, status: null,
statusReason: null, statusReason: null,
requestReason: null, requestReason: null,
requestStatus: null,
installed: false, installed: false,
installFailed: false,
}; };
export default class ClusterStore { export default class ClusterStore {
...@@ -49,6 +53,9 @@ export default class ClusterStore { ...@@ -49,6 +53,9 @@ export default class ClusterStore {
version: null, version: null,
chartRepo: 'https://gitlab.com/charts/gitlab-runner', chartRepo: 'https://gitlab.com/charts/gitlab-runner',
upgradeAvailable: null, upgradeAvailable: null,
updateAcknowledged: true,
updateSuccessful: false,
updateFailed: false,
}, },
prometheus: { prometheus: {
...applicationInitialState, ...applicationInitialState,
...@@ -93,6 +100,32 @@ export default class ClusterStore { ...@@ -93,6 +100,32 @@ export default class ClusterStore {
this.state.statusReason = reason; this.state.statusReason = reason;
} }
installApplication(appId) {
this.handleApplicationEvent(appId, INSTALL_EVENT);
}
notifyInstallFailure(appId) {
this.handleApplicationEvent(appId, APPLICATION_STATUS.ERROR);
}
updateApplication(appId) {
this.handleApplicationEvent(appId, UPDATE_EVENT);
}
notifyUpdateFailure(appId) {
this.handleApplicationEvent(appId, APPLICATION_STATUS.UPDATE_ERRORED);
}
handleApplicationEvent(appId, event) {
const currentAppState = this.state.applications[appId];
this.state.applications[appId] = transitionApplicationState(currentAppState, event);
}
acknowledgeSuccessfulUpdate(appId) {
this.state.applications[appId].updateAcknowledged = true;
}
updateAppProperty(appId, prop, value) { updateAppProperty(appId, prop, value) {
this.state.applications[appId][prop] = value; this.state.applications[appId][prop] = value;
} }
...@@ -109,12 +142,16 @@ export default class ClusterStore { ...@@ -109,12 +142,16 @@ export default class ClusterStore {
version, version,
update_available: upgradeAvailable, update_available: upgradeAvailable,
} = serverAppEntry; } = serverAppEntry;
const currentApplicationState = this.state.applications[appId] || {};
const nextApplicationState = transitionApplicationState(currentApplicationState, status);
this.state.applications[appId] = { this.state.applications[appId] = {
...(this.state.applications[appId] || {}), ...currentApplicationState,
status, ...nextApplicationState,
statusReason, statusReason,
installed: isApplicationInstalled(status), installed: isApplicationInstalled(nextApplicationState.status),
// Make sure uninstallable is always false until this feature is unflagged
uninstallable: false,
}; };
if (appId === INGRESS) { if (appId === INGRESS) {
......
...@@ -4,6 +4,11 @@ import { generateTreeList } from '../store/utils'; ...@@ -4,6 +4,11 @@ import { generateTreeList } from '../store/utils';
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
self.addEventListener('message', e => { self.addEventListener('message', e => {
const { data } = e; const { data } = e;
if (data === undefined) {
return;
}
const { treeEntries, tree } = generateTreeList(data); const { treeEntries, tree } = generateTreeList(data);
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
......
import $ from 'jquery'; import $ from 'jquery';
import { __ } from '~/locale';
export default class TransferDropdown { export default class TransferDropdown {
constructor() { constructor() {
...@@ -13,7 +14,7 @@ export default class TransferDropdown { ...@@ -13,7 +14,7 @@ export default class TransferDropdown {
} }
buildDropdown() { buildDropdown() {
const extraOptions = [{ id: '', text: 'No parent group' }, 'divider']; const extraOptions = [{ id: '', text: __('No parent group') }, 'divider'];
this.groupDropdown.glDropdown({ this.groupDropdown.glDropdown({
selectable: true, selectable: true,
......
...@@ -108,6 +108,7 @@ export default { ...@@ -108,6 +108,7 @@ export default {
:placeholder="placeholder" :placeholder="placeholder"
:value="text" :value="text"
class="note-textarea ide-commit-message-textarea" class="note-textarea ide-commit-message-textarea"
dir="auto"
name="commit-message" name="commit-message"
@scroll="handleScroll" @scroll="handleScroll"
@input="onInput" @input="onInput"
......
...@@ -74,7 +74,7 @@ export default { ...@@ -74,7 +74,7 @@ export default {
<gl-loading-icon <gl-loading-icon
v-if="isLoadingRepos" v-if="isLoadingRepos"
class="js-loading-button-icon import-projects-loading-icon" class="js-loading-button-icon import-projects-loading-icon"
:size="4" size="md"
/> />
<div v-else-if="hasProviderRepos || hasImportedProjects" class="table-responsive"> <div v-else-if="hasProviderRepos || hasImportedProjects" class="table-responsive">
<table class="table import-table"> <table class="table import-table">
......
...@@ -7,6 +7,8 @@ import mutations from './mutations'; ...@@ -7,6 +7,8 @@ import mutations from './mutations';
Vue.use(Vuex); Vue.use(Vuex);
export { state, actions, getters, mutations };
export default () => export default () =>
new Vuex.Store({ new Vuex.Store({
state: state(), state: state(),
......
...@@ -149,6 +149,7 @@ export default { ...@@ -149,6 +149,7 @@ export default {
v-model="descriptionText" v-model="descriptionText"
:data-update-url="updateUrl" :data-update-url="updateUrl"
class="hidden js-task-list-field" class="hidden js-task-list-field"
dir="auto"
> >
</textarea> </textarea>
......
...@@ -53,6 +53,7 @@ export default { ...@@ -53,6 +53,7 @@ export default {
v-model="formState.description" v-model="formState.description"
class="note-textarea js-gfm-input js-autosize markdown-area class="note-textarea js-gfm-input js-autosize markdown-area
qa-description-textarea" qa-description-textarea"
dir="auto"
data-supports-quick-actions="false" data-supports-quick-actions="false"
aria-label="Description" aria-label="Description"
placeholder="Write a comment or drag your files here…" placeholder="Write a comment or drag your files here…"
......
...@@ -20,6 +20,7 @@ export default { ...@@ -20,6 +20,7 @@ export default {
ref="input" ref="input"
v-model="formState.title" v-model="formState.title"
class="form-control qa-title-input" class="form-control qa-title-input"
dir="auto"
type="text" type="text"
placeholder="Title" placeholder="Title"
aria-label="Title" aria-label="Title"
......
...@@ -72,6 +72,7 @@ export default { ...@@ -72,6 +72,7 @@ export default {
'issue-realtime-trigger-pulse': pulseAnimation, 'issue-realtime-trigger-pulse': pulseAnimation,
}" }"
class="title" class="title"
dir="auto"
v-html="titleHtml" v-html="titleHtml"
></h2> ></h2>
<button <button
......
...@@ -135,6 +135,12 @@ function deferredInitialisation() { ...@@ -135,6 +135,12 @@ function deferredInitialisation() {
}); });
loadAwardsHandler(); loadAwardsHandler();
// Toggle Canary Badge
if (Cookies.get('gitlab_canary') && Cookies.get('gitlab_canary') === 'true') {
document.querySelector('.js-canary-badge').classList.remove('hidden');
document.querySelector('.js-canary-link').classList.add('hidden');
}
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
......
...@@ -351,6 +351,7 @@ Please check your network connection and try again.`; ...@@ -351,6 +351,7 @@ Please check your network connection and try again.`;
ref="textarea" ref="textarea"
slot="textarea" slot="textarea"
v-model="note" v-model="note"
dir="auto"
:disabled="isSubmitting" :disabled="isSubmitting"
name="note[note]" name="note[note]"
class="note-textarea js-vue-comment-form js-note-text class="note-textarea js-vue-comment-form js-note-text
......
...@@ -122,6 +122,7 @@ export default { ...@@ -122,6 +122,7 @@ export default {
v-model="note.note" v-model="note.note"
:data-update-url="note.path" :data-update-url="note.path"
class="hidden js-task-list-field" class="hidden js-task-list-field"
dir="auto"
></textarea> ></textarea>
<note-edited-text <note-edited-text
v-if="note.last_edited_at" v-if="note.last_edited_at"
......
...@@ -268,6 +268,7 @@ export default { ...@@ -268,6 +268,7 @@ export default {
:data-supports-quick-actions="!isEditing" :data-supports-quick-actions="!isEditing"
name="note[note]" name="note[note]"
class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input" class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
dir="auto"
aria-label="Description" aria-label="Description"
placeholder="Write a comment or drag your files here…" placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleKeySubmit()" @keydown.meta.enter="handleKeySubmit()"
......
...@@ -65,18 +65,18 @@ export default class ActivityCalendar { ...@@ -65,18 +65,18 @@ export default class ActivityCalendar {
this.daySize = 15; this.daySize = 15;
this.daySizeWithSpace = this.daySize + this.daySpace * 2; this.daySizeWithSpace = this.daySize + this.daySpace * 2;
this.monthNames = [ this.monthNames = [
'Jan', __('Jan'),
'Feb', __('Feb'),
'Mar', __('Mar'),
'Apr', __('Apr'),
'May', __('May'),
'Jun', __('Jun'),
'Jul', __('Jul'),
'Aug', __('Aug'),
'Sep', __('Sep'),
'Oct', __('Oct'),
'Nov', __('Nov'),
'Dec', __('Dec'),
]; ];
this.months = []; this.months = [];
this.firstDayOfWeek = firstDayOfWeek; this.firstDayOfWeek = firstDayOfWeek;
......
...@@ -34,6 +34,7 @@ export default () => { ...@@ -34,6 +34,7 @@ export default () => {
props: { props: {
isLoading: this.mediator.state.isLoading, isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline, pipeline: this.mediator.store.state.pipeline,
mediator: this.mediator,
}, },
on: { on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph, refreshPipelineGraph: this.requestRefreshPipelineGraph,
......
...@@ -30,6 +30,7 @@ export default { ...@@ -30,6 +30,7 @@ export default {
:id="inputId" :id="inputId"
:value="value" :value="value"
class="form-control js-gfm-input append-bottom-default commit-message-edit" class="form-control js-gfm-input append-bottom-default commit-message-edit"
dir="auto"
required="required" required="required"
rows="7" rows="7"
@input="$emit('input', $event.target.value)" @input="$emit('input', $event.target.value)"
......
...@@ -119,7 +119,8 @@ export default { ...@@ -119,7 +119,8 @@ export default {
}, },
showTargetBranchAdvancedError() { showTargetBranchAdvancedError() {
return Boolean( return Boolean(
this.mr.pipeline && this.mr.isOpen &&
this.mr.pipeline &&
this.mr.pipeline.target_sha && this.mr.pipeline.target_sha &&
this.mr.pipeline.target_sha !== this.mr.targetBranchSha, this.mr.pipeline.target_sha !== this.mr.targetBranchSha,
); );
......
...@@ -17,15 +17,13 @@ export default { ...@@ -17,15 +17,13 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
milestoneDue: this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null,
milestoneStart: this.milestone.start_date
? parsePikadayDate(this.milestone.start_date)
: null,
};
},
computed: { computed: {
milestoneDue() {
return this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null;
},
milestoneStart() {
return this.milestone.start_date ? parsePikadayDate(this.milestone.start_date) : null;
},
isMilestoneStarted() { isMilestoneStarted() {
if (!this.milestoneStart) { if (!this.milestoneStart) {
return false; return false;
......
<script> <script>
import '~/commons/bootstrap';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import IssueMilestone from '../../components/issue/issue_milestone.vue'; import IssueMilestone from '../../components/issue/issue_milestone.vue';
......
...@@ -17,10 +17,10 @@ export default { ...@@ -17,10 +17,10 @@ export default {
<template> <template>
<tr class="line_holder" :class="lineType"> <tr class="line_holder" :class="lineType">
<td class="diff-line-num old_line" :class="lineType"> <td class="diff-line-num old_line border-top-0 border-bottom-0" :class="lineType">
{{ line.old_line }} {{ line.old_line }}
</td> </td>
<td class="diff-line-num new_line" :class="lineType"> <td class="diff-line-num new_line border-top-0 border-bottom-0" :class="lineType">
{{ line.new_line }} {{ line.new_line }}
</td> </td>
<td class="line_content" :class="lineType"> <td class="line_content" :class="lineType">
......
...@@ -22,6 +22,7 @@ import noteHeader from '~/notes/components/note_header.vue'; ...@@ -22,6 +22,7 @@ import noteHeader from '~/notes/components/note_header.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import TimelineEntryItem from './timeline_entry_item.vue'; import TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils'; import { spriteIcon } from '../../../lib/utils/common_utils';
import initMRPopovers from '~/mr_popover/';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
...@@ -71,6 +72,9 @@ export default { ...@@ -71,6 +72,9 @@ export default {
); );
}, },
}, },
mounted() {
initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request'));
},
}; };
</script> </script>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import 'select2/select2'; import 'select2';
export default { export default {
name: 'Select2Select', name: 'Select2Select',
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
} }
&-body { &-body {
height: 120px; min-height: 120px;
&-warning { &-warning {
background-color: $orange-50; background-color: $orange-50;
...@@ -22,10 +22,8 @@ ...@@ -22,10 +22,8 @@
} }
} }
&-time-ago { &-icon {
&-icon { color: $gray-500;
color: $gray-500;
}
} }
&-footer { &-footer {
......
.toast-close {
font-size: $default-icon-size !important;
}
...@@ -447,30 +447,29 @@ ...@@ -447,30 +447,29 @@
} }
} }
.title-container,
.navbar-nav { .navbar-nav {
li { .badge.badge-pill {
.badge.badge-pill { position: inherit;
position: inherit; font-weight: $gl-font-weight-normal;
font-weight: $gl-font-weight-normal; margin-left: -6px;
margin-left: -6px; font-size: 11px;
font-size: 11px; color: $white-light;
color: $white-light; padding: 0 5px;
padding: 0 5px; line-height: 12px;
line-height: 12px; border-radius: 7px;
border-radius: 7px; box-shadow: 0 1px 0 rgba($gl-header-color, 0.2);
box-shadow: 0 1px 0 rgba($gl-header-color, 0.2);
&.green-badge {
&.issues-count { background-color: $green-500;
background-color: $green-500; }
}
&.merge-requests-count { &.merge-requests-count {
background-color: $orange-600; background-color: $orange-600;
} }
&.todos-count { &.todos-count {
background-color: $blue-500; background-color: $blue-500;
}
} }
} }
} }
......
...@@ -7,6 +7,9 @@ ...@@ -7,6 +7,9 @@
opacity: 1 !important; opacity: 1 !important;
* { * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none; user-select: none;
// !important to make sure no style can override this when dragging // !important to make sure no style can override this when dragging
cursor: grabbing !important; cursor: grabbing !important;
...@@ -256,6 +259,10 @@ ...@@ -256,6 +259,10 @@
} }
} }
.board-card-header {
text-align: initial;
}
.board-card-assignee { .board-card-assignee {
margin-top: -$gl-padding-4; margin-top: -$gl-padding-4;
margin-bottom: -$gl-padding-4; margin-bottom: -$gl-padding-4;
......
...@@ -60,6 +60,7 @@ ...@@ -60,6 +60,7 @@
overflow-wrap: break-word; overflow-wrap: break-word;
min-width: 0; min-width: 0;
width: 100%; width: 100%;
text-align: initial;
} }
.btn-edit { .btn-edit {
......
...@@ -456,7 +456,9 @@ ...@@ -456,7 +456,9 @@
// Don't hide the overflow in system messages // Don't hide the overflow in system messages
.system-note-message, .system-note-message,
.issuable-detail { .issuable-detail,
.md-preview-holder,
.note-body {
.scoped-label-wrapper { .scoped-label-wrapper {
.badge { .badge {
overflow: initial; overflow: initial;
......
...@@ -75,6 +75,8 @@ input[type='checkbox']:hover { ...@@ -75,6 +75,8 @@ input[type='checkbox']:hover {
} }
.search-input-wrap { .search-input-wrap {
width: 100%;
.search-icon, .search-icon,
.clear-icon { .clear-icon {
position: absolute; position: absolute;
...@@ -84,6 +86,9 @@ input[type='checkbox']:hover { ...@@ -84,6 +86,9 @@ input[type='checkbox']:hover {
.search-icon { .search-icon {
transition: color $default-transition-duration; transition: color $default-transition-duration;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none; user-select: none;
} }
......
...@@ -127,6 +127,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -127,6 +127,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
[ [
*::ApplicationSettingsHelper.visible_attributes, *::ApplicationSettingsHelper.visible_attributes,
*::ApplicationSettingsHelper.external_authorization_service_attributes, *::ApplicationSettingsHelper.external_authorization_service_attributes,
*lets_encrypt_visible_attributes,
:domain_blacklist_file, :domain_blacklist_file,
disabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [],
import_sources: [], import_sources: [],
...@@ -134,4 +135,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -134,4 +135,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
restricted_visibility_levels: [] restricted_visibility_levels: []
] ]
end end
def lets_encrypt_visible_attributes
return [] unless Feature.enabled?(:pages_auto_ssl)
[
:lets_encrypt_notification_email,
:lets_encrypt_terms_of_service_accepted
]
end
end end
...@@ -10,8 +10,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -10,8 +10,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics] before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index] before_action :expire_etag_cache, only: [:index]
before_action only: [:metrics, :additional_metrics] do before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:metrics_time_window) push_frontend_feature_flag(:metrics_time_window)
push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint)
end end
def index def index
...@@ -134,13 +135,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -134,13 +135,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
def metrics def metrics
# Currently, this acts as a hint to load the metrics details into the cache
# if they aren't there already
@metrics = environment.metrics || {}
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
# Currently, this acts as a hint to load the metrics details into the cache
# if they aren't there already
@metrics = environment.metrics || {}
render json: @metrics, status: @metrics.any? ? :ok : :no_content render json: @metrics, status: @metrics.any? ? :ok : :no_content
end end
end end
...@@ -156,6 +157,20 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -156,6 +157,20 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
end end
def metrics_dashboard
return render_403 unless Feature.enabled?(:environment_metrics_use_prometheus_endpoint, @project)
result = Gitlab::Metrics::Dashboard::Service.new(@project, @current_user, environment: environment).get_dashboard
respond_to do |format|
if result[:status] == :success
format.json { render status: :ok, json: result }
else
format.json { render status: result[:http_status], json: result }
end
end
end
def search def search
respond_to do |format| respond_to do |format|
format.json do format.json do
......
...@@ -56,6 +56,8 @@ module Projects ...@@ -56,6 +56,8 @@ module Projects
# overridden in EE # overridden in EE
def permitted_project_params def permitted_project_params
{ {
metrics_setting_attributes: [:external_dashboard_url],
error_tracking_setting_attributes: [ error_tracking_setting_attributes: [
:enabled, :enabled,
:api_host, :api_host,
......
...@@ -44,6 +44,12 @@ module Resolvers ...@@ -44,6 +44,12 @@ module Resolvers
alias_method :project, :object alias_method :project, :object
def resolve(**args) def resolve(**args)
# The project could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project to query for issues, so
# make sure it's loaded and not `nil` before continueing.
project.sync if project.respond_to?(:sync)
return Issue.none if project.nil?
# Will need to be be made group & namespace aware with # Will need to be be made group & namespace aware with
# https://gitlab.com/gitlab-org/gitlab-ce/issues/54520 # https://gitlab.com/gitlab-org/gitlab-ce/issues/54520
args[:project_id] = project.id args[:project_id] = project.id
......
...@@ -4,7 +4,7 @@ module BroadcastMessagesHelper ...@@ -4,7 +4,7 @@ module BroadcastMessagesHelper
def broadcast_message(message) def broadcast_message(message)
return unless message.present? return unless message.present?
content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do content_tag :div, dir: 'auto', class: 'broadcast-message', style: broadcast_message_style(message) do
icon('bullhorn') << ' ' << render_broadcast_message(message) icon('bullhorn') << ' ' << render_broadcast_message(message)
end end
end end
......
...@@ -4,7 +4,7 @@ require 'nokogiri' ...@@ -4,7 +4,7 @@ require 'nokogiri'
module MarkupHelper module MarkupHelper
include ActionView::Helpers::TagHelper include ActionView::Helpers::TagHelper
include ActionView::Context include ::Gitlab::ActionViewOutput::Context
def plain?(filename) def plain?(filename)
Gitlab::MarkupHelper.plain?(filename) Gitlab::MarkupHelper.plain?(filename)
......
...@@ -229,6 +229,16 @@ class ApplicationSetting < ApplicationRecord ...@@ -229,6 +229,16 @@ class ApplicationSetting < ApplicationRecord
presence: true, presence: true,
if: -> (setting) { setting.external_auth_client_cert.present? } if: -> (setting) { setting.external_auth_client_cert.present? }
validates :lets_encrypt_notification_email,
devise_email: true,
format: { without: /@example\.(com|org|net)\z/,
message: N_("Let's Encrypt does not accept emails on example.com") },
allow_blank: true
validates :lets_encrypt_notification_email,
presence: true,
if: :lets_encrypt_terms_of_service_accepted?
validates_with X509CertificateCredentialsValidator, validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert, certificate: :external_auth_client_cert,
pkey: :external_auth_client_key, pkey: :external_auth_client_key,
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Clusters module Clusters
module Applications module Applications
class Runner < ApplicationRecord class Runner < ApplicationRecord
VERSION = '0.4.0'.freeze VERSION = '0.4.1'.freeze
self.table_name = 'clusters_applications_runners' self.table_name = 'clusters_applications_runners'
......
...@@ -47,6 +47,12 @@ class Deployment < ApplicationRecord ...@@ -47,6 +47,12 @@ class Deployment < ApplicationRecord
Deployments::SuccessWorker.perform_async(id) Deployments::SuccessWorker.perform_async(id)
end end
end end
after_transition any => [:success, :failed, :canceled] do |deployment|
deployment.run_after_commit do
Deployments::FinishedWorker.perform_async(id)
end
end
end end
enum status: { enum status: {
...@@ -79,7 +85,16 @@ class Deployment < ApplicationRecord ...@@ -79,7 +85,16 @@ class Deployment < ApplicationRecord
end end
def cluster def cluster
project.deployment_platform(environment: environment.name)&.cluster platform = project.deployment_platform(environment: environment.name)
if platform.present? && platform.respond_to?(:cluster)
platform.cluster
end
end
def execute_hooks
deployment_data = Gitlab::DataBuilder::Deployment.build(self)
project.execute_services(deployment_data, :deployment_hooks)
end end
def last? def last?
......
...@@ -188,6 +188,7 @@ class Project < ApplicationRecord ...@@ -188,6 +188,7 @@ class Project < ApplicationRecord
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :project_repository, inverse_of: :project has_one :project_repository, inverse_of: :project
has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting' has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting'
# Merge Requests for target project should be removed with it # Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
...@@ -297,6 +298,7 @@ class Project < ApplicationRecord ...@@ -297,6 +298,7 @@ class Project < ApplicationRecord
reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? } reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
accepts_nested_attributes_for :error_tracking_setting, update_only: true accepts_nested_attributes_for :error_tracking_setting, update_only: true
accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
......
# frozen_string_literal: true
class ProjectMetricsSetting < ApplicationRecord
belongs_to :project
validates :external_dashboard_url,
length: { maximum: 255 },
addressable_url: { enforce_sanitization: true, ascii_only: true }
end
# frozen_string_literal: true
module ChatMessage
class DeploymentMessage < BaseMessage
attr_reader :commit_url
attr_reader :deployable_id
attr_reader :deployable_url
attr_reader :environment
attr_reader :short_sha
attr_reader :status
def initialize(data)
super
@commit_url = data[:commit_url]
@deployable_id = data[:deployable_id]
@deployable_url = data[:deployable_url]
@environment = data[:environment]
@short_sha = data[:short_sha]
@status = data[:status]
end
def attachments
[{
text: "#{project_link}\n#{deployment_link}, SHA #{commit_link}, by #{user_combined_name}",
color: color
}]
end
def activity
{}
end
private
def message
"Deploy to #{environment} #{humanized_status}"
end
def color
case status
when 'success'
'good'
when 'canceled'
'warning'
when 'failed'
'danger'
else
'#334455'
end
end
def project_link
link(project_name, project_url)
end
def deployment_link
link("Job ##{deployable_id}", deployable_url)
end
def commit_link
link(short_sha, commit_url)
end
def humanized_status
status == 'success' ? 'succeeded' : status
end
end
end
...@@ -33,7 +33,7 @@ class ChatNotificationService < Service ...@@ -33,7 +33,7 @@ class ChatNotificationService < Service
def self.supported_events def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push %w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page] pipeline wiki_page deployment]
end end
def fields def fields
...@@ -122,6 +122,8 @@ class ChatNotificationService < Service ...@@ -122,6 +122,8 @@ class ChatNotificationService < Service
ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page" when "wiki_page"
ChatMessage::WikiPageMessage.new(data) ChatMessage::WikiPageMessage.new(data)
when "deployment"
ChatMessage::DeploymentMessage.new(data)
end end
end end
......
...@@ -33,6 +33,11 @@ class DiscordService < ChatNotificationService ...@@ -33,6 +33,11 @@ class DiscordService < ChatNotificationService
# No-op. # No-op.
end end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields def default_fields
[ [
{ type: "text", name: "webhook", placeholder: "e.g. https://discordapp.com/api/webhooks/…" }, { type: "text", name: "webhook", placeholder: "e.g. https://discordapp.com/api/webhooks/…" },
......
...@@ -35,6 +35,11 @@ class HangoutsChatService < ChatNotificationService ...@@ -35,6 +35,11 @@ class HangoutsChatService < ChatNotificationService
'https://chat.googleapis.com/v1/spaces…' 'https://chat.googleapis.com/v1/spaces…'
end end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields def default_fields
[ [
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" }, { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
......
...@@ -33,6 +33,11 @@ class MicrosoftTeamsService < ChatNotificationService ...@@ -33,6 +33,11 @@ class MicrosoftTeamsService < ChatNotificationService
def default_channel_placeholder def default_channel_placeholder
end end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields def default_fields
[ [
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" }, { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
......
...@@ -1065,6 +1065,19 @@ class Repository ...@@ -1065,6 +1065,19 @@ class Repository
blob.data blob.data
end end
def create_if_not_exists
return if exists?
raw.create_repository
after_create
end
def blobs_metadata(paths, ref = 'HEAD')
references = Array.wrap(paths).map { |path| [ref, path] }
Gitlab::Git::Blob.batch_metadata(raw, references).map { |raw_blob| Blob.decorate(raw_blob) }
end
private private
# TODO Generice finder, later split this on finders by Ref or Oid # TODO Generice finder, later split this on finders by Ref or Oid
......
...@@ -50,6 +50,7 @@ class Service < ApplicationRecord ...@@ -50,6 +50,7 @@ class Service < ApplicationRecord
scope :job_hooks, -> { where(job_events: true, active: true) } scope :job_hooks, -> { where(job_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) } scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :deployment_hooks, -> { where(deployment_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults } scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
scope :deployment, -> { where(category: 'deployment') } scope :deployment, -> { where(category: 'deployment') }
...@@ -335,6 +336,8 @@ class Service < ApplicationRecord ...@@ -335,6 +336,8 @@ class Service < ApplicationRecord
"Event will be triggered when a wiki page is created/updated" "Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events" when "commit", "commit_events"
"Event will be triggered when a commit is created/updated" "Event will be triggered when a commit is created/updated"
when "deployment"
"Event will be triggered when a deployment finishes"
end end
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module UserStatusTooltip module UserStatusTooltip
extend ActiveSupport::Concern extend ActiveSupport::Concern
include ActionView::Helpers::TagHelper include ActionView::Helpers::TagHelper
include ActionView::Context include ::Gitlab::ActionViewOutput::Context
include EmojiHelper include EmojiHelper
include UsersHelper include UsersHelper
......
...@@ -12,7 +12,16 @@ module Projects ...@@ -12,7 +12,16 @@ module Projects
private private
def project_update_params def project_update_params
error_tracking_params error_tracking_params.merge(metrics_setting_params)
end
def metrics_setting_params
attribs = params[:metrics_setting_attributes]
return {} unless attribs
destroy = attribs[:external_dashboard_url].blank?
{ metrics_setting_attributes: attribs.merge(_destroy: destroy) }
end end
def error_tracking_params def error_tracking_params
......
...@@ -5,16 +5,33 @@ ...@@ -5,16 +5,33 @@
.form-group .form-group
= f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'label-bold' = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'label-bold'
= f.number_field :max_pages_size, class: 'form-control' = f.number_field :max_pages_size, class: 'form-control'
.form-text.text-muted 0 for unlimited .form-text.text-muted
= _("0 for unlimited")
.form-group .form-group
.form-check .form-check
= f.check_box :pages_domain_verification_enabled, class: 'form-check-input' = f.check_box :pages_domain_verification_enabled, class: 'form-check-input'
= f.label :pages_domain_verification_enabled, class: 'form-check-label' do = f.label :pages_domain_verification_enabled, class: 'form-check-label' do
Require users to prove ownership of custom domains = _("Require users to prove ownership of custom domains")
.form-text.text-muted .form-text.text-muted
Domain verification is an essential security measure for public GitLab = _("Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled")
sites. Users are required to demonstrate they control a domain before
it is enabled
= link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
- if Feature.enabled?(:pages_auto_ssl)
%h5
= _("Configure Let's Encrypt")
%p
- lets_encrypt_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: "https://letsencrypt.org/" }
= _("%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA), that give digital certificates in order to enable HTTPS (SSL/TLS) for websites.").html_safe % { lets_encrypt_link_start: lets_encrypt_link_start, lets_encrypt_link_end: '</a>'.html_safe }
.form-group
= f.label :lets_encrypt_notification_email, _("Email"), class: 'label-bold'
= f.text_field :lets_encrypt_notification_email, class: 'form-control'
.form-text.text-muted
= _("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.")
.form-group
.form-check
= f.check_box :lets_encrypt_terms_of_service_accepted, class: 'form-check-input'
= f.label :lets_encrypt_terms_of_service_accepted, class: 'form-check-label' do
// Terms of Service should actually be a link, but the best way to get the url is using API
// So it will be done in later MR
= _("I have read and agree to the Let's Encrypt Terms of Service")
= f.submit 'Save changes', class: "btn btn-success" = f.submit _('Save changes'), class: "btn btn-success"
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
.col-sm-10 .col-sm-10
= f.text_area :message, class: "form-control js-autosize", = f.text_area :message, class: "form-control js-autosize",
required: true, required: true,
dir: 'auto',
data: { preview_path: preview_admin_broadcast_messages_path } data: { preview_path: preview_admin_broadcast_messages_path }
.form-group.row.js-toggle-colors-container .form-group.row.js-toggle-colors-container
.col-sm-10.offset-sm-2 .col-sm-10.offset-sm-2
......
...@@ -11,7 +11,8 @@ ...@@ -11,7 +11,8 @@
= link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip event-target-link append-right-4', title: event.target_title do = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip event-target-link append-right-4', title: event.target_title do
= event.target.reference_link_text = event.target.reference_link_text
- unless event.milestone? - unless event.milestone?
%span.event-target-title.append-right-4= "&quot;".html_safe + event.target.title + "&quot".html_safe %span.event-target-title.append-right-4{ dir: "auto" }
= "&quot;".html_safe + event.target.title + "&quot".html_safe
- else - else
%span.event-type.d-inline-block.append-right-4{ class: event.action_name } %span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event_action_name(event) = event_action_name(event)
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
%span.event-type.d-inline-block.append-right-4{ class: event.action_name } %span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event.action_name = event.action_name
= event_note_title_html(event) = event_note_title_html(event)
%span.event-target-title.append-right-4= "&quot;".html_safe + event.target.title + "&quot".html_safe %span.event-target-title.append-right-4{ dir: "auto" }
= "&quot;".html_safe + event.target.title + "&quot".html_safe
= render "events/event_scope", event: event = render "events/event_scope", event: event
......
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
- if logo_text.present? - if logo_text.present?
%span.logo-text.d-none.d-lg-block.prepend-left-8 %span.logo-text.d-none.d-lg-block.prepend-left-8
= logo_text = logo_text
%span.js-canary-badge.badge.badge-pill.green-badge.align-self-center
= _('Next')
- if current_user - if current_user
= render "layouts/nav/dashboard" = render "layouts/nav/dashboard"
...@@ -38,7 +40,7 @@ ...@@ -38,7 +40,7 @@
= link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('issues', size: 16) = sprite_icon('issues', size: 16)
- issues_count = assigned_issuables_count(:issues) - issues_count = assigned_issuables_count(:issues)
%span.badge.badge-pill.issues-count{ class: ('hidden' if issues_count.zero?) } %span.badge.badge-pill.issues-count.green-badge{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count) = number_with_delimiter(issues_count)
- if header_link?(:merge_requests) - if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do = nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
......
...@@ -7,3 +7,6 @@ ...@@ -7,3 +7,6 @@
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback" = link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile) - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
= render 'shared/user_dropdown_contributing_link' = render 'shared/user_dropdown_contributing_link'
- if Gitlab.com?
%li.js-canary-link
= link_to _("Switch to GitLab Next"), "https://next.gitlab.com/"
...@@ -80,6 +80,8 @@ ...@@ -80,6 +80,8 @@
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
= deleted_message % { project_name: fork_source_name(@project) } = deleted_message % { project_name: fork_source_name(@project) }
= render_if_exists "projects/home_mirror"
- if @project.badges.present? - if @project.badges.present?
.project-badges.mb-2 .project-badges.mb-2
- @project.badges.each do |badge| - @project.badges.each do |badge|
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
= f.text_area attr, = f.text_area attr,
class: classes, class: classes,
placeholder: placeholder, placeholder: placeholder,
dir: 'auto',
data: { supports_quick_actions: supports_quick_actions, data: { supports_quick_actions: supports_quick_actions,
supports_autocomplete: supports_autocomplete } supports_autocomplete: supports_autocomplete }
- else - else
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
.issuable-info-container .issuable-info-container
.issuable-main-info .issuable-main-info
.issue-title.title .issue-title.title
%span.issue-title-text %span.issue-title-text{ dir: "auto" }
- if issue.confidential? - if issue.confidential?
%span.has-tooltip{ title: _('Confidential') } %span.has-tooltip{ title: _('Confidential') }
= confidential_icon(issue) = confidential_icon(issue)
......
...@@ -37,21 +37,21 @@ ...@@ -37,21 +37,21 @@
%ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs %ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs
%li.notes-tab.qa-notes-tab %li.notes-tab.qa-notes-tab
= tab_link_for @merge_request, :show, force_link: @commit.present? do = tab_link_for @merge_request, :show, force_link: @commit.present? do
Discussion = _("Discussion")
%span.badge.badge-pill= @merge_request.related_notes.user.count %span.badge.badge-pill= @merge_request.related_notes.user.count
- if @merge_request.source_project - if @merge_request.source_project
%li.commits-tab %li.commits-tab
= tab_link_for @merge_request, :commits do = tab_link_for @merge_request, :commits do
Commits = _("Commits")
%span.badge.badge-pill= @commits_count %span.badge.badge-pill= @commits_count
- if @pipelines.any? - if @pipelines.any?
%li.pipelines-tab %li.pipelines-tab
= tab_link_for @merge_request, :pipelines do = tab_link_for @merge_request, :pipelines do
Pipelines = _("Pipelines")
%span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
%li.diffs-tab.qa-diffs-tab %li.diffs-tab.qa-diffs-tab
= tab_link_for @merge_request, :diffs do = tab_link_for @merge_request, :diffs do
Changes = _("Changes")
%span.badge.badge-pill= @merge_request.diff_size %span.badge.badge-pill= @merge_request.diff_size
.d-inline-flex.flex-wrap .d-inline-flex.flex-wrap
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@merge_request), #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@merge_request),
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
= link_to _('Read more'), help_page_path('workflow/repository_mirroring'), target: '_blank' = link_to _('Read more'), help_page_path('workflow/repository_mirroring'), target: '_blank'
.settings-content .settings-content
= form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'false', data: mirrors_form_data_attributes } do |f| = form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'new-password', data: mirrors_form_data_attributes } do |f|
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
%h3.panel-title= _('Mirror a repository') %h3.panel-title= _('Mirror a repository')
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
.form-group.has-feedback .form-group.has-feedback
= label_tag :url, _('Git repository URL'), class: 'label-light' = label_tag :url, _('Git repository URL'), class: 'label-light'
= text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+" = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password'
= render 'projects/mirrors/instructions' = render 'projects/mirrors/instructions'
......
...@@ -21,10 +21,9 @@ ...@@ -21,10 +21,9 @@
- if @scope == 'projects' - if @scope == 'projects'
.term .term
= render 'shared/projects/list', projects: @search_objects, pipeline_status: false = render 'shared/projects/list', projects: @search_objects, pipeline_status: false
- elsif %w[blobs wiki_blobs].include?(@scope)
= render partial: 'search/results/blob', collection: @search_objects, locals: { projects: blob_projects(@search_objects) }
- else - else
= render partial: "search/results/#{@scope.singularize}", collection: @search_objects - locals = { projects: blob_projects(@search_objects) } if %w[blobs wiki_blobs].include?(@scope)
= render partial: "search/results/#{@scope.singularize}", collection: @search_objects, locals: locals
- if @scope != 'projects' - if @scope != 'projects'
= paginate_collection(@search_objects) = paginate_collection(@search_objects)
- project = find_project_for_result_blob(projects, wiki_blob) - project = find_project_for_result_blob(projects, wiki_blob)
- wiki_blob = parse_search_result(wiki_blob) - wiki_blob = parse_search_result(wiki_blob)
- wiki_blob_link = project_wiki_path(project, wiki_blob.basename) - wiki_blob_link = project_wiki_path(project, Pathname.new(wiki_blob.filename).sub_ext(''))
= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: wiki_blob.filename, blob_link: wiki_blob_link } = render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: wiki_blob.filename, blob_link: wiki_blob_link }
%a.toggle-sidebar-button.js-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" } %a.toggle-sidebar-button.js-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
= sprite_icon('angle-double-left', css_class: 'icon-angle-double-left') = sprite_icon('angle-double-left', css_class: 'icon-angle-double-left')
= sprite_icon('angle-double-right', css_class: 'icon-angle-double-right') = sprite_icon('angle-double-right', css_class: 'icon-angle-double-right')
%span.collapse-text _("Collapse sidebar") %span.collapse-text= _("Collapse sidebar")
= button_tag class: 'close-nav-button', type: 'button' do = button_tag class: 'close-nav-button', type: 'button' do
= sprite_icon('close', size: 16) = sprite_icon('close', size: 16)
%span.collapse-text _("Close sidebar") %span.collapse-text= _("Close sidebar")
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%div{ class: div_class } %div{ class: div_class }
= form.text_field :title, required: true, maxlength: 255, autofocus: true, = form.text_field :title, required: true, maxlength: 255, autofocus: true,
autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title') autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title'), dir: 'auto'
- if issuable.respond_to?(:work_in_progress?) - if issuable.respond_to?(:work_in_progress?)
.form-text.text-muted .form-text.text-muted
......
...@@ -83,6 +83,7 @@ ...@@ -83,6 +83,7 @@
- pipeline_processing:ci_build_schedule - pipeline_processing:ci_build_schedule
- deployment:deployments_success - deployment:deployments_success
- deployment:deployments_finished
- repository_check:repository_check_clear - repository_check:repository_check_clear
- repository_check:repository_check_batch - repository_check:repository_check_batch
......
# frozen_string_literal: true
module Deployments
class FinishedWorker
include ApplicationWorker
queue_namespace :deployment
def perform(deployment_id)
Deployment.find_by_id(deployment_id).try(:execute_hooks)
end
end
end
...@@ -21,8 +21,10 @@ class PostReceive ...@@ -21,8 +21,10 @@ class PostReceive
if repo_type.wiki? if repo_type.wiki?
process_wiki_changes(post_received) process_wiki_changes(post_received)
else elsif repo_type.project?
process_project_changes(post_received) process_project_changes(post_received)
else
# Other repos don't have hooks for now
end end
end end
......
---
title: Enable Sidekiq Reliable Fetcher for background jobs by default
merge_request: 27530
author:
type: added
---
title: Hide ScopedBadge overflow notes
merge_request: 27651
author:
type: fixed
---
title: Add .NET Core YAML template
merge_request: 25604
author: Piotr Wosiek
type: added
---
title: Adds badge for Canary environment and help link
merge_request:
author:
type: added
---
title: Add auto direction for issue title
merge_request: 27378
author: Ahmad Haghighi
type: fixed
---
title: Display a toast message when the Kubernetes runner has successfully upgraded.
merge_request: 27206
author:
type: changed
---
title: Add backend support for a External Dashboard URL setting
merge_request: 27550
author:
type: added
---
title: Only show the "target branch has advanced" message when the merge request is
open
merge_request: 27588
author:
type: fixed
---
title: Fix Kubernetes service template deployment jobs broken as of 11.10.0
merge_request: 27687
author:
type: fixed
---
title: Fix bug where system note MR has no popover
merge_request: 27747
author:
type: fixed
---
title: Resolve Misalignment on suggested changes diff table
merge_request: 27612
author:
type: fixed
---
title: Show proper wiki links in search results
merge_request: 27634
author:
type: fixed
---
title: Add deployment events to chat notification services
merge_request: 27338
author:
type: added
---
title: Add gitaly session id & catfile-cache feature flag
merge_request: 27472
author:
type: performance
---
title: 'refactor(issue): Refactored issue tests from Karma to Jest'
merge_request: 27673
author: Martin Hobert
type: other
---
title: 'Refactored notes tests from Karma to Jest'
merge_request: 27648
author: Martin Hobert
type: other
---
title: Disable password autocomplete in mirror repository form
merge_request: 27542
author:
type: fixed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment