New upstream version 11.10.4+dfsg

This commit is contained in:
Utkarsh Gupta 2019-05-18 00:54:41 +05:30
parent b93a71aa47
commit 688aff3489
4154 changed files with 238775 additions and 63884 deletions

View file

@ -9,5 +9,6 @@
/scripts/ /scripts/
/tmp/ /tmp/
/vendor/ /vendor/
jest.config.js
karma.config.js karma.config.js
webpack.config.js webpack.config.js

View file

@ -9,9 +9,6 @@ plugins:
- import - import
- html - html
settings: settings:
html/html-extensions:
- '.html'
- '.html.raw'
import/resolver: import/resolver:
webpack: webpack:
config: './config/webpack.config.js' config: './config/webpack.config.js'

View file

@ -1,5 +1,8 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.18-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.18-chrome-71.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29"
include:
- local: /lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml
.dedicated-runner: &dedicated-runner .dedicated-runner: &dedicated-runner
retry: 1 retry: 1
tags: tags:
@ -66,6 +69,7 @@ stages:
paths: paths:
- knapsack/ - knapsack/
- rspec_flaky/ - rspec_flaky/
- rspec_profiling/
.use-pg: &use-pg .use-pg: &use-pg
services: services:
@ -159,6 +163,7 @@ stages:
- coverage/ - coverage/
- knapsack/ - knapsack/
- rspec_flaky/ - rspec_flaky/
- rspec_profiling/
- tmp/capybara/ - tmp/capybara/
reports: reports:
junit: junit_rspec.xml junit: junit_rspec.xml
@ -244,8 +249,8 @@ package-and-qa:
- ./scripts/trigger-build omnibus - ./scripts/trigger-build omnibus
when: manual when: manual
only: only:
- //@gitlab-org/gitlab-ce - /.+/@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee - /.+/@gitlab-org/gitlab-ee
# Review docs base # Review docs base
.review-docs: &review-docs .review-docs: &review-docs
@ -336,6 +341,7 @@ retrieve-tests-metadata:
- wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}' - '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- mkdir -p rspec_flaky/ - mkdir -p rspec_flaky/
- mkdir -p rspec_profiling/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH - wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}' - '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
@ -350,7 +356,7 @@ update-tests-metadata:
- rspec_flaky/ - rspec_flaky/
policy: push policy: push
script: script:
- retry gem install fog-aws mime-types activesupport --no-document - retry gem install fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH} - FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH}
@ -358,6 +364,7 @@ update-tests-metadata:
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH' - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json - rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json
- scripts/insert-rspec-profiling-data
flaky-examples-check: flaky-examples-check:
<<: *dedicated-runner <<: *dedicated-runner
@ -388,13 +395,11 @@ flaky-examples-check:
.assets-compile-cache: &assets-compile-cache .assets-compile-cache: &assets-compile-cache
cache: cache:
key: "assets-compile:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v4" key: "assets-compile:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v5"
paths: paths:
- vendor/ruby/ - vendor/ruby/
- .yarn-cache/ - .yarn-cache/
# We have disabled caching of sprockets for now, as it fails to pick up changes in SCSS: - tmp/cache/assets/sprockets
# https://gitlab.com/gitlab-org/gitlab-ce/issues/57431
# - tmp/cache/assets/sprockets
compile-assets: compile-assets:
<<: *dedicated-runner <<: *dedicated-runner
@ -446,6 +451,17 @@ setup-test-env:
- master - master
- /(^docs[\/-].*|.*-docs$)/ - /(^docs[\/-].*|.*-docs$)/
.review-schedules-only: &review-schedules-only
only:
refs:
- schedules@gitlab-org/gitlab-ce
- schedules@gitlab-org/gitlab-ee
kubernetes: active
except:
refs:
- tags
- /(^docs[\/-].*|.*-docs$)/
.review-base: &review-base .review-base: &review-base
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-no-db-pull-cache-job
<<: *review-only <<: *review-only
@ -475,6 +491,9 @@ setup-test-env:
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/
@ -511,6 +530,7 @@ rspec-mysql:
parallel: 50 parallel: 50
.rspec-quarantine: &rspec-quarantine .rspec-quarantine: &rspec-quarantine
retry: 0
script: script:
- export CACHE_CLASSES=true - export CACHE_CLASSES=true
- scripts/gitaly-test-spawn - scripts/gitaly-test-spawn
@ -665,13 +685,43 @@ gitlab:assets:compile:
- public/assets/ - public/assets/
<<: *assets-compile-cache <<: *assets-compile-cache
only: only:
- //@gitlab-org/gitlab-ce - /.+/@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee - /.+/@gitlab-org/gitlab-ee
- //@gitlab/gitlabhq - /.+/@gitlab/gitlabhq
- //@gitlab/gitlab-ee - /.+/@gitlab/gitlab-ee
tags: tags:
- gitlab-org-delivery - docker
- high-cpu - gitlab-org
gitlab:ui:visual:
tags:
- gitlab-org
before_script: []
allow_failure: true
dependencies:
- compile-assets
script:
# Remove node modules from GitLab that may conflict with gitlab-ui
- rm -r node_modules
- git clone https://gitlab.com/gitlab-org/gitlab-ui.git
- cp public/assets/application-*.css gitlab-ui/styles/application.css
- cd gitlab-ui
- yarn install
- CSS_URL=./application.css yarn test
only:
changes:
- app/assets/stylesheets/*.scss
- app/assets/stylesheets/**/*.scss
- app/assets/stylesheets/**/**/*.scss
except:
refs:
- /(^docs[\/-].*|.*-docs$)/
- master
variables:
- $CI_COMMIT_MESSAGE =~ /\[skip visual\]/i
artifacts:
paths:
- tests/__image_snapshots__/
karma: karma:
<<: *dedicated-no-docs-pull-cache-job <<: *dedicated-no-docs-pull-cache-job
@ -727,31 +777,14 @@ jest:
code_quality: code_quality:
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
allow_failure: true
# gitlab-org runners set `privileged: false` but we need to have it set to true # gitlab-org runners set `privileged: false` but we need to have it set to true
# since we're using Docker in Docker # since we're using Docker in Docker
tags: [] tags: []
before_script: [] before_script: []
services:
- docker:stable-dind
variables:
SETUP_DB: "false"
DOCKER_DRIVER: overlay2
cache: {} cache: {}
dependencies: [] dependencies: []
script: variables:
# Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products SETUP_DB: "false"
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run
--env SOURCE_CODE="$PWD"
--volume "$PWD":/code
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts:
reports:
codequality: gl-code-quality-report.json
expire_in: 1 week
sast: sast:
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-no-db-pull-cache-job
@ -819,8 +852,6 @@ qa:selectors:
.qa-frontend-node: &qa-frontend-node .qa-frontend-node: &qa-frontend-node
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-no-db-pull-cache-job
stage: test stage: test
variables:
NODE_OPTIONS: --max_old_space_size=3584
cache: cache:
key: "$CI_JOB_NAME" key: "$CI_JOB_NAME"
paths: paths:
@ -957,18 +988,16 @@ no_ee_check:
script: script:
- scripts/no-ee-check - scripts/no-ee-check
only: only:
- //@gitlab-org/gitlab-ce - /.+/@gitlab-org/gitlab-ce
# GitLab Review apps # GitLab Review apps
review-build-cng: .review-build-cng-base: &review-build-cng-base
<<: *review-only
image: ruby:2.5-alpine image: ruby:2.5-alpine
stage: test stage: test
before_script: [] before_script: []
dependencies: [] dependencies: []
cache: {} cache: {}
variables: variables:
GIT_DEPTH: "1"
API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}" API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
script: script:
- apk add --update openssl curl jq - apk add --update openssl curl jq
@ -977,12 +1006,18 @@ review-build-cng:
- wait_for_job_to_be_done "gitlab:assets:compile" - 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-deploy: review-build-cng:
<<: *review-only
<<: *review-build-cng-base
schedule:review-build-cng:
<<: *review-schedules-only
<<: *review-build-cng-base
.review-deploy-base: &review-deploy-base
<<: *review-base <<: *review-base
retry: 2
allow_failure: true allow_failure: true
variables: variables:
GIT_DEPTH: "1"
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"
@ -999,13 +1034,17 @@ review-deploy:
- source ./scripts/review_apps/review-apps.sh - source ./scripts/review_apps/review-apps.sh
script: script:
- wait_for_job_to_be_done "review-build-cng" - wait_for_job_to_be_done "review-build-cng"
- check_kube_domain - perform_review_app_deployment
- download_gitlab_chart
- ensure_namespace review-deploy:
- install_tiller <<: *review-deploy-base
- install_external_dns
- time deploy schedule:review-deploy:
- add_license <<: *review-deploy-base
<<: *review-schedules-only
script:
- wait_for_job_to_be_done "schedule:review-build-cng"
- perform_review_app_deployment
.review-qa-base: &review-qa-base .review-qa-base: &review-qa-base
<<: *review-docker <<: *review-docker
@ -1033,19 +1072,39 @@ review-deploy:
- apk update && apk add curl jq - apk update && apk add curl jq
- source ./scripts/review_apps/review-apps.sh - source ./scripts/review_apps/review-apps.sh
- 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}}
- wait_for_job_to_be_done "review-deploy"
review-qa-smoke: review-qa-smoke:
<<: *review-qa-base <<: *review-qa-base
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
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 when: manual
.review-performance-base: &review-performance-base
<<: *review-qa-base
script:
- wait_for_job_to_be_done "review-deploy"
- mkdir -p gitlab-exporter
- wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- mkdir 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"
- mv sitespeed-results/data/performance.json performance.json
artifacts:
paths:
- sitespeed-results/
reports:
performance: performance.json
review-performance:
<<: *review-performance-base
review-stop: review-stop:
<<: *review-base <<: *review-base
<<: *single-script-job <<: *single-script-job
@ -1065,21 +1124,21 @@ review-stop:
schedule:review-cleanup: schedule:review-cleanup:
<<: *review-base <<: *review-base
<<: *review-schedules-only
stage: build stage: build
allow_failure: true allow_failure: true
variables: variables:
GIT_DEPTH: "1" GIT_DEPTH: "1"
environment: environment:
name: review/auto-cleanup name: review/auto-cleanup
only:
refs:
- schedules@gitlab-org/gitlab-ce
- schedules@gitlab-org/gitlab-ee
kubernetes: active
except:
- tags
- /(^docs[\/-].*|.*-docs$)/
before_script: before_script:
- gem install gitlab --no-document - gem install gitlab --no-document
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"

View file

@ -1,6 +1,6 @@
# Backend Maintainers are the default for all ruby files # Backend Maintainers are the default for all ruby files
*.rb @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @nick.thomas @rspeicher @rymai @smcgivern *.rb @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @nick.thomas @rspeicher @rymai @smcgivern
*.rake @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @nick.thomas @rspeicher @rymai @smcgivern *.rake @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @nick.thomas @rspeicher @rymai @smcgivern
# Technical writing team are the default reviewers for everything in `doc/` # Technical writing team are the default reviewers for everything in `doc/`
/doc/ @axil @marcia /doc/ @axil @marcia

View file

@ -0,0 +1,32 @@
#### Database Reviewer Checklist
Thank you for becoming a ~database reviewer! Please work on the list below to complete your setup. For any question, reach out to #database an mention @abrandl.
- [ ] Change issue title to include your name: `Database Reviewer Checklist: Your Name`
- [ ] Review general [code review guide](https://docs.gitlab.com/ee/development/code_review.html)
- [ ] Review [database review documentation](https://about.gitlab.com/handbook/engineering/workflow/code-review/database.html)
- [ ] Familiarize with [migration helpers](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/database/migration_helpers.rb) and review usage in existing migrations
- [ ] Read [database migration style guide](https://docs.gitlab.com/ee/development/migration_style_guide.html)
- [ ] Familiarize with best practices in [database guides](https://docs.gitlab.com/ee/development/#database-guides)
- [ ] Watch [Optimising Rails Database Queries: Episode 1](https://www.youtube.com/watch?v=79GurlaxhsI)
- [ ] Read [Understanding EXPLAIN plans](https://docs.gitlab.com/ee/development/understanding_explain_plans.html)
- [ ] Review [database best practices](https://docs.gitlab.com/ee/development/#best-practices)
- [ ] Review how we use [database instances restored from a backup](https://ops.gitlab.net/gitlab-com/gl-infra/gitlab-restore/postgres-gprd) for testing and make sure you're set up to execute pipelines (check [README.md](https://ops.gitlab.net/gitlab-com/gl-infra/gitlab-restore/postgres-gprd/blob/master/README.md) and reach out to @abrandl since this is currently subject to being changed)
- [ ] Get yourself added to [@gl-database](https://gitlab.com/groups/gl-database/-/group_members) group and respond to @-mentions to the group (reach out to any maintainer on the group to get added). You will get TODOs on gitlab.com for group mentions.
- [ ] Make sure you have proper access to at least a read-only replica in staging and production
- [ ] Indicate in `data/team.yml` your role as a database reviewer ([example MR](https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/19600/diffs)). Assign MR to your manager for merge.
- [ ] Send one MR to improve the [review documentation](https://about.gitlab.com/handbook/engineering/workflow/code-review/database.html) or the [issue template](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/issue_templates/Database%20Reviewer.md)
Note that *approving and accepting* merge requests is *restricted* to
Database Maintainers only. As a reviewer, pass the MR to a maintainer
for approval.
You're all set! Watch out for TODOs on GitLab.com.
###### Where to go for questions?
Reach out to `#database` on Slack and mention @abrandl for any questions.
cc @abrandl
/label ~meta ~database

View file

@ -0,0 +1,20 @@
<!-- This issue requests a technical writer review as required for documentation
content that was merged without one. -->
<!-- NOTE: Please add a DevOps stage label (format `devops:<stage_name>`)
and assign the technical writer who is
[listed for that stage](https://about.gitlab.com/handbook/product/categories/#devops-stages). -->
## References
Merged MR that introduced documentation requiring review:
Related issue(s):
## Further Details
<!-- Any additional context, questions, or notes for the technical writer. -->
/label ~Documentation ~docs-review

View file

@ -1,54 +1,53 @@
<!--See the general documentation guidelines https://docs.gitlab.com/ee/development/documentation --> <!--
<!-- Mention "documentation" or "docs" in the issue title --> * Use this issue template for suggesting new docs or updates to existing docs.
Note: Doc work as part of feature development is covered in the Feature Request template.
<!-- Use this description template for new docs or updates to existing docs. --> * For issues related to features of the docs.gitlab.com site, see
https://gitlab.com/gitlab-com/gitlab-docs/issues/
<!-- Check the documentation structure guidelines for guidance: https://docs.gitlab.com/ee/development/documentation/structure.html--> * For information about documentation content and process, see
https://docs.gitlab.com/ee/development/documentation/ -->
- [ ] Documents Feature A <!-- feature name --> <!-- Type of issue -->
- [ ] Follow-up from: #XXX, !YYY <!-- Mention related issues, MRs, and epics when available -->
## New doc or update? <!-- Un-comment the line for the applicable doc issue type to add its label.
Note that all text on that line is deleted upon issue creation. -->
<!-- /label ~"docs:fix" - Correction or clarification needed. -->
<!-- /label ~"docs:new" - New doc needed to cover a new topic or use case. -->
<!-- /label ~"docs:improvement" - Improving an existing doc; e.g. adding a diagram, adding or rewording text, resolving redundancies, cross-linking, etc. -->
<!-- /label ~"docs:revamp" - Review a page or group of pages in order to plan and implement major improvements/rewrites. -->
<!-- /label ~"docs:other" - Anything else. -->
<!-- Mark either of these boxes: --> ### Problem to solve
- [ ] New documentation <!-- Include the following detail as necessary:
- [ ] Update existing documentation * What product or feature(s) affected?
* What docs or doc section affected? Include links or paths.
* Is there a problem with a specific document, or a feature/process that's not addressed sufficiently in docs?
* Any other ideas or requests?
-->
## Checklists ### Further details
### Product Manager <!--
* Any concepts, procedures, reference info we could add to make it easier to successfully use GitLab?
* Include use cases, benefits, and/or goals for this work.
* If adding content: What audience is it intended for? (What roles and scenarios?)
For ideas, see personas at https://design.gitlab.com/research/personas or the persona labels at
https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A
-->
<!-- Reference: https://docs.gitlab.com/ee/development/documentation/workflow.html#1-product-manager-s-role-in-the-documentation-process --> ### Proposal
- [ ] Add the correct labels <!-- Further specifics for how can we solve the problem. -->
- [ ] Add the correct milestone
- [ ] Indicate the correct document/directory for this feature <!-- (ping the tech writers for help if you're not sure) -->
- [ ] Fill the doc blurb below
#### Documentation blurb ### Who can address the issue
<!-- Documentation template: https://docs.gitlab.com/ee/development/documentation/structure.html#documentation-template-for-new-docs --> <!-- What if any special expertise is required to resolve this issue? -->
- Doc **title** ### Other links/references
<!-- write the doc title here --> <!-- E.g. related GitLab issues/MRs -->
- Feature **overview/description**
<!-- Write the feature overview here -->
- Feature **use cases**
<!-- Write the use cases here -->
### Developer
<!-- Reference: https://docs.gitlab.com/ee/development/documentation/workflow.html#2-developer-s-role-in-the-documentation-process -->
- [ ] Copy the doc blurb above and paste it into the doc
- [ ] Write the tutorial - explain how to use the feature
- [ ] Submit the MR using the appropriate MR description template
/label ~Documentation /label ~Documentation

View file

@ -1,45 +1,36 @@
### Problem to solve ### Problem to solve
<!--- What problem do we solve? --> <!-- What problem do we solve? -->
### Target audience ### Intended users
<!--- For whom are we doing this? Include a [persona](https://design.gitlab.com/research/personas) <!-- Who will use this feature? If known, include any of the following: types of users (e.g. Developer), personas, or specific company roles (e.g. Release Manager). It's okay to write "Unknown" and fill this field in later.
listed below, if applicable, along with its [label](https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A), Personas can be found at https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/ -->
or define a specific company role, e.g. "Release Manager".
Existing personas are: (copy relevant personas out of this comment, and delete any persona that does not apply)
- Parker, Product Manager, https://design.gitlab.com/research/personas#persona-parker
/label ~"Persona: Product Manager"
- Delaney, Development Team Lead, https://design.gitlab.com/research/personas#persona-delaney
/label ~"Persona: Development Team Lead"
- Sasha, Software Developer, https://design.gitlab.com/research/personas#persona-sasha
/label ~"Persona: Software developer"
- Devon, DevOps Engineer, https://design.gitlab.com/research/personas#persona-devon
/label ~"Persona: DevOps Engineer"
- Sidney, Systems Administrator, https://design.gitlab.com/research/personas#persona-sidney
/label ~"Persona: Systems Administrator"
- Sam, Security Analyst, https://design.gitlab.com/research/personas#persona-sam
/label ~"Persona: Security Analyst"
-->
### Further details ### Further details
<!--- Include use cases, benefits, and/or goals (contributes to our vision?) --> <!-- Include use cases, benefits, and/or goals (contributes to our vision?) -->
### Proposal ### Proposal
<!--- How are we going to solve the problem? Try to include the user journey! --> <!-- How are we going to solve the problem? Try to include the user journey! https://about.gitlab.com/handbook/journeys/#user-journey -->
### Permissions and Security
<!-- What permissions are required to perform the described actions? Are they consistent with the existing permissions as documented for users, groups, and projects as appropriate? Is the proposed behavior consistent between the UI, API, and other access methods (e.g. email replies)? -->
### Documentation
<!-- See the Feature Change Documentation Workflow https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html
Add all known Documentation Requirements here, per https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html#documentation-requirements -->
### Testing
<!-- What risks does this change pose? How might it affect the quality of the product? What additional test coverage or changes to tests will be needed? Will it require cross-browser testing? See the test engineering process for further guidelines: https://about.gitlab.com/handbook/engineering/quality/guidelines/test-engineering/ -->
### What does success look like, and how can we measure that? ### What does success look like, and how can we measure that?
<!--- Define both the success metrics and acceptance criteria. Note that success metrics indicate the desired business outcomes, while acceptance criteria indicate when the solution is working correctly. If there is no way to measure success, link to an issue that will implement a way to measure this --> <!-- Define both the success metrics and acceptance criteria. Note that success metrics indicate the desired business outcomes, while acceptance criteria indicate when the solution is working correctly. If there is no way to measure success, link to an issue that will implement a way to measure this. -->
### Links / references ### Links / references

View file

@ -30,6 +30,7 @@ Set the title to: `Description of the original issue`
#### Documentation and final details #### Documentation and final details
- [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links) - [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
- [ ] Add links to this issue and your MRs in the description of the security release issue
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details) - [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details) - [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details) - [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)

View file

@ -93,4 +93,4 @@ When adding new automated tests, please keep [testing levels](https://docs.gitla
in mind. in mind.
--> -->
/label ~Quality ~"test plan" /label ~Quality ~"test\-plan"

View file

@ -26,7 +26,7 @@ https://docs.gitlab.com/ce/development/documentation/index.html#changing-documen
to the new document if there are any Disqus comments on the old document thread. to the new document if there are any Disqus comments on the old document thread.
- [ ] Update the link in `features.yml` (if applicable) - [ ] Update the link in `features.yml` (if applicable)
- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE - [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE
with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee). with the changes as well (https://docs.gitlab.com/ce/development/documentation/index.html#cherry-picking-from-ce-to-ee).
- [ ] Ping one of the technical writers for review. - [ ] Ping one of the technical writers for review.
/label ~Documentation /label ~Documentation

View file

@ -16,7 +16,7 @@ Add a description of your merge request here.
## Database checklist ## Database checklist
- [ ] Conforms to the [database guides](https://docs.gitlab.com/ee/development/README.html#databases-guides) - [ ] Conforms to the [database guides](https://docs.gitlab.com/ee/development/README.html#database-guides)
When adding migrations: When adding migrations:
@ -49,10 +49,10 @@ When removing columns, tables, indexes or other structures:
## General checklist ## General checklist
- [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary - [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary
- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/documentation/index.html#contributing-to-docs) - [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/documentation/)
- [ ] [Tests added for this feature/bug](https://docs.gitlab.com/ee/development/testing_guide/index.html) - [ ] [Tests added for this feature/bug](https://docs.gitlab.com/ee/development/testing_guide/index.html)
- [ ] Conforms to the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html) - [ ] Conforms to the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
- [ ] Conforms to the [merge request performance guidelines](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html) - [ ] Conforms to the [merge request performance guidelines](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
- [ ] Conforms to the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides) - [ ] Conforms to the [style guides](https://docs.gitlab.com/ee/development/contributing/style_guides.html)
/label ~database /label ~database

View file

@ -1,33 +1,39 @@
<!--See the general documentation guidelines https://docs.gitlab.com/ee/development/documentation --> <!-- Follow the documentation workflow https://docs.gitlab.com/ee/development/documentation/workflow.html -->
<!-- Additional information is located at https://docs.gitlab.com/ee/development/documentation/ -->
<!-- Mention "documentation" or "docs" in the MR title --> <!-- Mention "documentation" or "docs" in the MR title -->
<!-- For changing documentation location use the "Change documentation location" template -->
<!-- Use this description template for new docs or updates to existing docs. For changing documentation location use the "Change documentation location" template -->
## What does this MR do? ## What does this MR do?
<!-- Briefly describe what this MR is about --> <!-- Briefly describe what this MR is about. -->
## Related issues ## Related issues
<!-- Mention the issue(s) this MR closes or is related to --> <!-- Link related issues below. Insert the issue link or reference after the word "Closes" if merging this should automatically close it. -->
Closes
## Author's checklist ## Author's checklist
- [ ] [Apply the correct labels and milestone](https://docs.gitlab.com/ee/development/documentation/workflow.html#2-developer-s-role-in-the-documentation-process) - [ ] Follow the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html).
- [ ] Crosslink the document from the higher-level index - [ ] Link docs to and from the higher-level index page, plus other related docs where helpful.
- [ ] Crosslink the document from other subject-related docs - [ ] Apply the ~Documentation label.
- [ ] Feature moving tiers? Make sure the change is also reflected in [`features.yml`](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml)
- [ ] Correctly apply the product [badges](https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges) and [tiers](https://docs.gitlab.com/ee/development/documentation/styleguide.html#gitlab-versions-and-tiers)
- [ ] [Port the MR to EE (or backport from CE)](https://docs.gitlab.com/ee/development/documentation/index.html#cherry-picking-from-ce-to-ee): _always recommended, required when the `ee-compat-check` job fails_
## Review checklist ## Review checklist
- [ ] Your team's review (required) All reviewers can help ensure accuracy, clarity, completeness, and adherence to the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html).
- [ ] PM's review (recommended, but not a blocker)
- [ ] Technical writer's review (required) **1. Primary Reviewer**
- [ ] Merge the EE-MR first, CE-MR afterwards
* [ ] Review by a code reviewer or other selected colleague to confirm accuracy, clarity, and completeness. This can be skipped for minor fixes without substantive content changes.
**2. Technical Writer**
* [ ] Optional: Technical writer review. If not requested for this MR, must be scheduled post-merge. To request for this MR, assign the writer listed for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages).
**3. Maintainer**
1. [ ] Review by assigned maintainer, who can always request/require the above reviews. Maintainer's review can occur before or after a technical writer review.
1. [ ] Ensure a release milestone is set and that you merge the equivalent EE MR before the CE MR if both exist.
1. [ ] If there has not been a technical writer review, [create an issue for one using the Doc Review template](https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issuable_template=Doc%20Review).
/label ~Documentation /label ~Documentation

View file

@ -7,6 +7,10 @@ See [the general developer security release guidelines](https://gitlab.com/gitla
This merge request _must not_ close the corresponding security issue _unless_ it This merge request _must not_ close the corresponding security issue _unless_ it
targets master. targets master.
When submitting a merge request for CE, a corresponding EE merge request is
always required. This makes it easier to merge security merge requests, as
manually merging CE into EE is no longer required.
--> -->
## Related issues ## Related issues
@ -20,8 +24,8 @@ targets master.
- [ ] Title of this MR is the same as for all backports - [ ] Title of this MR is the same as for all backports
- [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security` - [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security`
- [ ] Add a link to this MR in the `links` section of related issue - [ ] Add a link to this MR in the `links` section of related issue
- [ ] Add a link to an EE MR if required - [ ] Set up an EE MR (always required for CE merge requests): EE_MR_LINK_HERE
- [ ] Assign to a reviewer - [ ] Assign to a reviewer (that is not a release manager)
## Reviewer checklist ## Reviewer checklist

View file

@ -1,13 +1,5 @@
{ {
"printWidth": 100, "printWidth": 100,
"singleQuote": true, "singleQuote": true,
"trailingComma": "es5",
"overrides": [
{
"files": ["**/app/**/*", "**/spec/**/*"],
"options": {
"trailingComma": "all" "trailingComma": "all"
} }
}
]
}

View file

@ -8,6 +8,7 @@ require:
- rubocop-rspec - rubocop-rspec
AllCops: AllCops:
TargetRubyVersion: 2.5
TargetRailsVersion: 5.0 TargetRailsVersion: 5.0
Exclude: Exclude:
- 'vendor/**/*' - 'vendor/**/*'
@ -145,6 +146,20 @@ Naming/FileName:
- XSS - XSS
- GRPC - GRPC
Rails/ApplicationRecord:
Enabled: true
Exclude:
# Models in database migrations should not subclass from ApplicationRecord
# as they need to be as decoupled from application code as possible
- db/**/*.rb
- lib/gitlab/background_migration/**/*.rb
- lib/gitlab/database/**/*.rb
- spec/**/*.rb
- ee/db/**/*.rb
- ee/lib/gitlab/background_migration/**/*.rb
- ee/lib/ee/gitlab/background_migration/**/*.rb
- ee/spec/**/*.rb
# GitLab ################################################################### # GitLab ###################################################################
Gitlab/ModuleWithInstanceVariables: Gitlab/ModuleWithInstanceVariables:
@ -181,3 +196,11 @@ Cop/InjectEnterpriseEditionModule:
Exclude: Exclude:
- 'spec/**/*' - 'spec/**/*'
- 'ee/spec/**/*' - 'ee/spec/**/*'
Style/ReturnNil:
Enabled: true
# It isn't always safe to replace `=~` with `.match?`, especially when there are
# nil values on the left hand side
Performance/RegexpMatch:
Enabled: false

View file

@ -1,7 +1,9 @@
# Linter Documentation: # Linter Documentation:
# https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md # https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md
scss_files: 'app/assets/stylesheets/**/*.scss' scss_files:
- 'app/assets/stylesheets/**/*.scss'
- 'ee/app/assets/stylesheets/**/*.scss'
exclude: exclude:
- 'app/assets/stylesheets/pages/emojis.scss' - 'app/assets/stylesheets/pages/emojis.scss'

111
.stylelintrc Normal file
View file

@ -0,0 +1,111 @@
{
"plugins":[
"./scripts/frontend/stylelint/stylelint-duplicate-selectors.js",
"./scripts/frontend/stylelint/stylelint-utility-classes.js",
"stylelint-scss",
],
"rules":{
"at-rule-blacklist":[
"debug"
],
"at-rule-no-unknown":null,
"at-rule-no-vendor-prefix":true,
"block-no-empty":true,
"block-opening-brace-space-before":"always",
"color-hex-case":"lower",
"color-hex-length":"short",
"color-named":"never",
"color-no-invalid-hex":true,
"declaration-bang-space-after":"never",
"declaration-bang-space-before":"always",
"declaration-block-semicolon-newline-after":"always",
"declaration-block-semicolon-space-before":"never",
"declaration-block-single-line-max-declarations":1,
"declaration-block-trailing-semicolon":"always",
"declaration-colon-space-after":"always-single-line",
"declaration-colon-space-before":"never",
"declaration-property-value-blacklist":{
"border":[
"none"
],
"border-top":[
"none"
],
"border-right":[
"none"
],
"border-bottom":[
"none"
],
"border-left":[
"none"
]
},
"function-comma-space-after":"always-single-line",
"function-parentheses-space-inside":"never",
"function-url-quotes":"always",
"indentation":2,
"length-zero-no-unit":true,
"max-nesting-depth":[
3,
{
"ignoreAtRules":[
"each",
"media",
"supports",
"include"
],
"severity":"warning"
}
],
"media-feature-name-no-vendor-prefix":true,
"media-feature-parentheses-space-inside":"never",
"no-missing-end-of-source-newline":true,
"number-leading-zero":"always",
"number-no-trailing-zeros":true,
"property-no-unknown":true,
"property-no-vendor-prefix": [true, { "ignoreProperties": ["user-select"] }],
"rule-empty-line-before":[
"always-multi-line",
{
"except":[
"first-nested"
],
"ignore":[
"after-comment"
]
}
],
"scss/at-extend-no-missing-placeholder":[true,{ "severity": "warning" }],
"scss/at-function-pattern":"^[a-z]+([a-z0-9-]+[a-z0-9]+)?$",
"scss/at-import-no-partial-leading-underscore":true,
"scss/at-import-partial-extension-blacklist":[
"scss"
],
"scss/at-mixin-pattern":"^[a-z]+([a-z0-9-]+[a-z0-9]+)?$",
"scss/at-rule-no-unknown":true,
"scss/dollar-variable-colon-space-after":"always",
"scss/dollar-variable-colon-space-before":"never",
"scss/dollar-variable-pattern":"^[_]?[a-z]+([a-z0-9-]+[a-z0-9]+)?$",
"scss/percent-placeholder-pattern":"^[a-z]+([a-z0-9-]+[a-z0-9]+)?$",
"scss/selector-no-redundant-nesting-selector":true,
"selector-class-pattern":[
"^[a-z0-9\\-]+$",
{
"message":"Selector should be written in lowercase with hyphens (selector-class-pattern)",
"severity": "warning"
},
],
"selector-list-comma-newline-after":"always",
"selector-max-compound-selectors":[3, { "severity": "warning" }],
"selector-max-id":1,
"selector-no-vendor-prefix":true,
"selector-pseudo-element-colon-notation":"double",
"selector-pseudo-element-no-unknown":true,
"shorthand-property-no-redundant-values":true,
"string-quotes":"single",
"value-no-vendor-prefix":[true, { ignoreValues: ["sticky"] }],
"stylelint-gitlab/duplicate-selectors":[true,{ "severity": "warning" }],
"stylelint-gitlab/utility-classes":[true,{ "severity": "warning" }],
}
}

View file

@ -2,63 +2,627 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 11.8.9 (2019-04-25) ## 11.10.4 (2019-05-01)
### Security (5 changes) ### Fixed (12 changes)
- Improve credentials sanitization on repository mirror integration. !3078 - Fix MR popover on ToDos page. !27382
- Stop sending emails to users who can't read commit. - Fix 500 in general pipeline settings when passing an invalid build timeout. !27416
- Fix bug where system note MR has no popover. !27589
- Fix bug when project export to remote url fails. !27614
- `on_stop` is not automatically triggered with pipelines for merge requests. !27618
- Update Workhorse to v8.5.2. !27631
- Show proper wiki links in search results. !27634
- Make `CI_COMMIT_REF_NAME` and `SLUG` variable idempotent. !27663
- Fix Kubernetes service template deployment jobs broken as of 11.10.0. !27687
- Prevent text selection when dragging in issue boards. !27724
- Fix pipelines for merge requests does not show pipeline page when source branch is removed. !27803
- Fix Metrics Environments dropdown.
### Performance (2 changes)
- Prevent concurrent execution of PipelineScheduleWorker. !27781
- Fix slow performance with compiling HAML templates. !27782
## 11.10.3 (2019-04-30)
### Security (1 change)
- Allow to see project events only with api scope token.
## 11.10.2 (2019-04-25)
### Security (4 changes)
- Loosen regex for exception sanitization. !3076
- Resolve: moving an issue to private repo leaks namespace and project name.
- Escape path in new merge request mail. - Escape path in new merge request mail.
- Only allow modification of content when note is edited. - Stop sending emails to users who can't read commit.
- Upgrade Rails to 5.0.7.2.
## 11.8.8 (2019-04-23) ## 11.10.1 (2019-04-23)
### Fixed (5 changes) ### Fixed (2 changes)
- Bring back Rugged implementation of find_commit. !25477 - Upgrade Gitaly to 1.34.0. !27494
- Fix bug in BitBucket imports with SHA shorter than 40 chars. !26050 - Fix filtering of labels from system note link. !27507
- Fix health checks not working behind load balancers. !26055
- Fix error creating a merge request when diff includes a null byte. !26190 ### Changed (1 change)
- Avoid excessive recursive calls with Rugged TreeEntries. !26813
- Disable just-in-time Kubernetes resource creation for project level clusters. !27352
### Performance (1 change) ### Performance (1 change)
- Bring back Rugged implementation of ListCommitsByOid. !27441 - Bring back Rugged implementation of ListCommitsByOid. !27441
### Other (4 changes) ### Other (1 change)
- Bring back Rugged implementation of GetTreeEntries. !25674 - Bump required Ruby version check to 2.5.3. !27495
- Bring back Rugged implementation of CommitIsAncestor. !25702
- Bring back Rugged implementation of TreeEntry. !25706
- Bring back Rugged implementation of commit_tree_entry. !25896
## 11.8.7 (2019-04-09) ## 11.10.0 (2019-04-22)
- No changes. ### Security (9 changes)
## 11.8.6 (2019-03-28) - Update Rails to 5.0.7.2. !27022
- Disallow guest users from accessing Releases.
- Return cached languages if they've been detected before.
- Added rake task for removing EXIF data from existing uploads.
- Disallow updating namespace when updating a project.
- Fix XSS in resolve conflicts form.
- Hide "related branches" when user does not have permission.
- Fix PDF.js vulnerability.
- Use UntrustedRegexp for matching refs policy.
### Security (7 changes) ### Fixed (81 changes, 21 of them are from the community)
- Update `border-radius` of form controls and remove extra space above page titles. !24497
- Disallow reopening of a locked merge request. !24882 (Jan Beckmann)
- Align EmailValidator to validate_email gem implementation. !24971 (Horatiu Eugen Vlad)
- add a uniq constraints on issues and mrs labels. !25435 (Antoine Huret)
- Display draft when toggling replies. !25563
- Fix markdown table header and table content borders. !25666
- Fix authorized application count. !25715 (moyuru)
- Added "Add List" checkbox to create label dropdown to make creation of list optional. !25716 (Tucker Chapman)
- Makes emoji picker full width on mobile. !25883 (Jacopo Beschi @jacopo-beschi)
- Don't cutoff letters in MR and Issue links. !25910 (gfyoung)
- Fix unwanted character replacement on project members page caused by usage of sanitize function. !25946 (Elias Werberich)
- Fix UI for closed MR when source project is removed. !25967 (Takuya Noguchi)
- Keep inline as much as possible in system notes on issuable. !25968 (Takuya Noguchi)
- Fixes long review app subdomains. !25990 (walkafwalka)
- Fix counting of groups in admin dashboard. !26009
- Disable inaccessible navigation links upon archiving a project. !26020 (Elias Werberich)
- Fixed - Create project label window is cut off at the bottom. !26049
- Fix error shown when loading links to specific comments. !26092
- Fix group transfer selection possibilities. !26123 (Peter Marko)
- Fix UI layout on Commits on mobile. !26133 (Takuya Noguchi)
- Fix continuous bitbucket import loading spinner. !26175
- Resolves Branch name is lost if I change commit mode in Web IDE. !26180
- Fix removing remote mirror failure which leaves unnecessary refs behind. !26213
- Fix Error 500 when user commits Wiki page with no commit message. !26247
- Handle missing keys in sentry api response. !26264
- Implemented whitespace-trimming for file names in Web IDE. !26270
- Fix misalignment of group overview page buttons. !26292
- Reject HEAD requests to info/refs endpoint. !26334
- Prevent namespace dropdown in new project form from exceeding container. !26343
- Fix hover animation consistency in top navbar items. !26345
- Exclude system notes from commits in merge request discussions. !26396
- Resolve Code in other column of side-by-side diff is highlighted when selecting code on one side. !26423
- Prevent fade out transition on loading-button component. !26428
- Fix merge commits being used as default squash commit messages. !26445
- Expand resolved discussion when linking to a comment in the discussion. !26483
- Show statistics also when repository is disabled. !26509 (Peter Marko)
- Fix multiple series queries on metrics dashboard. !26514
- Releases will now be automatically deleted when deleting corresponding tag. !26530
- Make stylistic improvements to diff nav header. !26557
- Clear pipeline status cache after destruction of pipeline. !26575
- Update fugit which fixes a potential infinite loop. !26579
- Fixes job link in artifacts page breadcrumb. !26592
- Fix quick actions add label name middle word overlaps. !26602 (Jacopo Beschi @jacopo-beschi)
- Fix Auto DevOps missing domain error handling. !26627
- Fix jupyter rendering bug that ended in an infinite loop. !26656 (ROSPARS Benoit)
- Use a fixed git abbrev parameter when we fetch a git revision. !26707
- Enabled text selection highlighting in diffs in Web IDE. !26721 (Isaac Smith)
- Remove `path` and `branch` labels from metrics. !26744
- Resolve "Hide Kubernetes cluster warning if project has cluster related". !26749
- Fix long label overflow on metrics dashboard. !26775
- Group transfer now properly redirects to edit on failure. !26837
- Only execute system hooks once when pushing tags. !26888
- Fix UI anchor links after docs refactor. !26890
- Fix MWPS does not work for merge request pipelines. !26906
- Create pipelines for merge requests only when source branch is updated. !26921
- Fix notfication emails having wrong encoding. !26931
- Allow task lists that follow a blockquote to work correctly. !26937
- Fix image diff swipe view on commit and compare pages. !26968 (ftab)
- Fix IDE detection of MR from fork with same branch name. !26986
- Fix single string values for the 'include' keyword validation of gitlab-ci.yml. !26998 (Paul Bonaud (@paulrbr))
- Do not display Ingress IP help text when there isnt an Ingress IP assigned. !27057
- Fix real-time updates for projects that contain a reserved word. !27060
- Remove duplicates from issue related merge requests. !27067
- Add to white-space nowrap to all buttons. !27069
- Handle possible HTTP exception for Sentry client. !27080
- Guard against nil dereferenced_target. !27192
- Update GitLab Workhorse to v8.5.1. !27217
- Fix long file header names bug in diffs. !27233
- Always return the deployment in the UpdateDeploymentService#execute method. !27322
- Fix remove_source_branch merge request API handling. !27392
- Fixed bug with hashes in urls in WebIDE. !54376 (Kieran Andrews)
- Fix bug where MR popover doesn't go away on mouse leave.
- Only consider active milestones when using the special Started milestone filter.
- Scroll to diff file content when clicking on file header name and it is not a link to other page.
- Remove non-functional add issue button on welcome list.
- Fixed expand full file button showing on images.
- Fixed Web IDE web workers not working with relative URLs.
- Fixed Web IDE not loading merge request files.
- Fixed duplicated diff too large error message.
- Fixed sticky headers in merge request creation diffs.
- Fix bug when reopening milestone from index page.
### Deprecated (1 change)
- Allow to use untrusted Regexp via feature flag. !26905
### Changed (35 changes, 4 of them are from the community)
- Create MR pipelines with `refs/merge-requests/:iid/head`. !25504
- Create Kubernetes resources for projects when their deployment jobs run. !25586
- Remove unnecessary folder prefix from environment name. !25600
- Update deploy boards to additionally select on "app.gitlab.com" annotations. !25623
- Allow failed custom hook script errors to safely appear in GitLab UI by filtering error messages by the prefix GL-HOOK-ERR:. !25625
- Add link on two-factor authorization settings page to leave group that enforces two-factor authorization. !25731
- Reduce height of instance system header and footer. !25752
- Unify behaviour of 'Copy commit SHA to clipboard' to use full commit SHA. !25829 (Max Winterstein)
- Show loading spinner while Ingress/Knative IP is being assigned. !25912
- Hashed Storage: Prevent a migration and rollback running at the same time. !25976
- Make time counters show 'just now' for everything under one minute. !25992 (Sergiu Marton)
- Allow filtering labels list by one or two characters. !26012
- Implements the creation strategy for multi-line suggestions. !26057
- Automate base domain help text on Clusters page. !26124
- Set user.name limit to 128 characters. !26146
- Update gitlab-markup to 1.7.0 which requies python3. !26246
- Update system message banner font size to 12px. !26293
- Extend timezone dropdown. !26311
- Upgrade to Gitaly v1.29.0. !26406
- Automatically set Prometheus step interval. !26441
- Knative version bump 0.2.2 -> 0.3.0. !26459 (Chris Baumbauer)
- Display cluster form validation error messages inline. !26502
- Split Auto-DevOps.gitlab-ci.yml into reusable templates. !26520
- Update spinners in group list component. !26572
- Allow removing last owner from subgroup if parent group has owners. !26718
- Check mergeability in MergeToRefService. !26757
- Show download diff links for closed MRs. !26772
- Fix Container Scanning in Kubernetes Runners. !26793
- Move "Authorize project access with external service" to Core. !26823
- Localize notifications dropdown. !26844
- Order labels alphabetically in issue boards. !26927
- Upgrade to Gitaly v1.32.0. !26989
- Upgrade to Gitaly v1.33.0. !27065
- collapse file tree by default if the merge request changes only one file. (Riccardo Padovani <riccardo@rpadovani.com>)
- Removes the undescriptive CI Charts header.
### Performance (17 changes)
- Drop legacy artifacts usage as there are no leftovers. !24294
- Cache Repository#root_ref within a request. !25903
- Allow ref name caching CommitService#find_commit. !26248
- Avoid loading pipeline status in project search. !26342
- Fix some N+1s in loading routes and counting members for groups in @-autocomplete. !26491
- GitHub import: Run housekeeping after initial import. !26600
- Add initial complexity limits to GraphQL queries. !26629
- Cache FindCommit results in pipelines view. !26776
- Fix and expand Gitaly FindCommit caching. !27018
- Enable FindCommit caching for project and commits pages. !27048
- Expand FindCommit caching to blob and refs. !27084
- Enable Gitaly FindCommit caching for TreeController. !27100
- Improve performance of PR import. !27121
- Process at most 4 pipelines during push. !27205
- Disable method instrumentation for diffs. !27235
- Speed up filtering issues in a project when searching.
- Speed up generation of avatar URLs when using object storage.
### Added (35 changes, 6 of them are from the community)
- Add users search results to global search. !21197 (Alexis Reigel)
- Add target branch filter to merge requests search bar. !24380 (Hiroyuki Sato)
- Add Knative metrics to Prometheus. !24663 (Chris Baumbauer <cab@cabnetworks.net>)
- Support multi-line suggestions. !25211
- Allow to sort wiki pages by date and title. !25365
- Allow external diffs to be used conditionally. !25432
- Add usage counts for error tracking feature. !25472
- Enable/disable Auto DevOps at the Group level. !25533
- Update pipeline list view to accommodate post-merge pipeline information. !25690
- GraphQL Types can be made to always authorize access to resources of that Type. !25724
- Update clair-local-scan to 2.0.6. !25743 (Takuya Noguchi)
- Update pipeline block on merge request page to accommodate post-merge pipeline information. !25745
- Support multiple queries per chart on metrics dash. !25758
- Update pipeline detail view to accommodate post-merge pipelines. !25775
- Update job detail sidebar to accommodate post-merge pipeline information. !25777
- Add merge request pipeline flag to pipeline entity. !25846
- Expose group id on home panel. !25897 (Peter Marko)
- Move allow developers to create projects in groups to Core. !25975
- Add two new warning messages to the MR widget about merge request pipelines. !25983
- Support installing Group runner on group-level cluster. !26260
- Improve the Knative installation on Clusters. !26339
- Show error when namespace/svc account missing. !26362
- Add select by title to milestones API. !26573
- Implemented support for creation of new files from URL in Web IDE. !26622
- Add control for masking variable values in runner logs. !26751
- Allow merge requests to be created via git push options. !26752
- Create a shortcut for a new MR in the Web IDE. !26792
- Allow reactive caching to be used in services. !26839
- Add a Prometheus API per environment. !26841
- Allow merge requests to be set to merge when pipeline succeeds via git push options. !26842
- Use gitlabktl to build and deploy GitLab Serverless Functions. !26926
- Make touch events work on image diff swipe view and onion skin. !26971 (ftab)
- Add extended merge request tooltip.
- Added prometheus monitoring to GraphQL.
- Adding highest role property to admin's user details page.
### Other (29 changes, 6 of them are from the community)
- Update rack-oauth2 1.2.1 -> 1.9.3. !17868
- Merge the gitlab-shell "gitlab-keys" functionality into GitLab CE. !25598
- Refactor all_pipelines in Merge request. !25676
- Show error backtrace when logging errors to kubernetes.log. !25726
- Apply recaptcha API change in 4.0. !25921 (Praveen Arimbrathodiyil)
- Remove fake repository_path response. !25942 (Fabio Papa)
- Use curl silent/show-error options on Auto DevOps. !25954 (Takuya Noguchi)
- Explicitly set master_auth setting to enable basic auth and client certificate for new GKE clusters. !26018
- Project: Improve empty repository state UI. !26024
- Externalize strings from `/app/views/projects/pipelines`. !26035 (George Tsiolis)
- Prepare multi-line suggestions for rendering in Markdown. !26107
- Improve mobile UI on User Profile page. !26240 (Takuya Noguchi)
- Update GitLab Runner Helm Chart to 0.3.0/11.9.0. !26467
- Improve project merge request settings. !26495
- Bump kubectl to 1.11.9 and Helm to 2.13.1 in Auto-DevOps.gitlab-ci.yml. !26534
- Upgrade bootstrap_form Gem. !26568
- Add API access check to Graphql. !26570
- Change project avatar remove button to a link. !26589
- Log Gitaly RPC duration to api_json.log and production_json.log. !26652
- Add cluster domain to Project Cluster API. !26735
- Move project tags to separate line. !26797
- Changed button label at /pipelines/new. !26893 (antfobe,leonardofl)
- Update GitLab Shell to v9.0.0. !27002
- Migrate clusters tests to jest. !27013
- Rewrite related MRs widget with Vue. !27027
- Restore HipChat project service. !27172
- Externalize admin deploy keys strings.
- Removes EE differences for environments_table.vue.
- Removes EE differences for environment_item.vue.
## 11.9.6 (2019-04-04)
### Fixed (3 changes)
- Force to recreate all MR diffs on import. !26480
- Fix API /project/:id/branches not returning correct merge status. !26785
- Avoid excessive recursive calls with Rugged TreeEntries. !26813
### Performance (1 change)
- Force a full GC after importing a project. !26803
## 11.9.5 (2019-04-03)
### Fixed (3 changes)
- Force to recreate all MR diffs on import. !26480
- Fix API /project/:id/branches not returning correct merge status. !26785
- Avoid excessive recursive calls with Rugged TreeEntries. !26813
### Performance (1 change)
- Force a full GC after importing a project. !26803
## 11.9.3 (2019-03-27)
### Security (8 changes)
- Disallow guest users from accessing Releases. - Disallow guest users from accessing Releases.
- Fix PDF.js vulnerability. - Fix PDF.js vulnerability.
- Hide "related branches" when user does not have permission. - Hide "related branches" when user does not have permission.
- Fix XSS in resolve conflicts form. - Fix XSS in resolve conflicts form.
- Added rake task for removing EXIF data from existing uploads. - Added rake task for removing EXIF data from existing uploads.
- Return cached languages if they've been detected before.
- Disallow updating namespace when updating a project. - Disallow updating namespace when updating a project.
- Use UntrustedRegexp for matching refs policy. - Use UntrustedRegexp for matching refs policy.
## 11.8.5 (2019-03-27) ## 11.9.2 (2019-03-26)
- Unreleased due to QA failure. ### Security (8 changes)
## 11.8.4 (2019-03-26) - Disallow guest users from accessing Releases.
- Fix PDF.js vulnerability.
- Hide "related branches" when user does not have permission.
- Fix XSS in resolve conflicts form.
- Added rake task for removing EXIF data from existing uploads.
- Return cached languages if they've been detected before.
- Disallow updating namespace when updating a project.
- Use UntrustedRegexp for matching refs policy.
## 11.9.1 (2019-03-25)
### Fixed (7 changes)
- Fix issue that caused the "Show all activity" button to appear on top of the mini pipeline status dropdown on the merge request page. !26274
- Fix duplicated bottom match line on merge request parallel diff view. !26402
- Allow users who can push to protected branches to create protected branches via CLI. !26413
- Add missing .gitlab-ci.yml to Android template. !26415
- Refresh commit count after repository head changes. !26473
- Set proper default-branch for repository on GitHub Import. !26476
- GitHub importer: Use the project creator to create branches from forks. !26510
### Changed (1 change)
- Upgrade to Gitaly v1.27.1. !26533
## 11.9.0 (2019-03-22)
### Security (24 changes)
- Use encrypted runner tokens. !25532
- Stop linking to unrecognized package sources. !55518
- Disable issue boards API when issues are disabled.
- Forbid creating discussions for users with restricted access.
- Fix leaking private repository information in API.
- Fixed ability to see private groups by users not belonging to given group.
- Prevent releases links API to leak tag existance.
- Display the correct number of MRs a user has access to.
- Block local URLs for Kubernetes integration.
- Fix arbitrary file read via diffs during import.
- Check if desired milestone for an issue is available.
- Don't allow non-members to see private related MRs.
- Check snippet attached file to be moved is within designated directory.
- Fix blind SSRF in Prometheus integration by checking URL before querying.
- Fix git clone revealing private repo's presence.
- Remove project serialization in quick actions response.
- Don't show new issue link after move when a user does not have permissions.
- Limit mermaid rendering to 5K characters.
- Show only merge requests visible to user on milestone detail page.
- Display only information visible to current user on the Milestone page.
- Do not display impersonated sessions under active sessions and remove ability to revoke session.
- Validate session key when authorizing with GCP to create a cluster.
- Do not disclose milestone titles for unauthorized users.
- Remove the possibility to share a project with a group that a user is not a member of.
### Removed (1 change)
- Remove HipChat integration from GitLab. !22223
### Fixed (86 changes, 21 of them are from the community)
- Fixes issue with AWS V4 signatures not working with some S3 providers. !21788
- Validate 'include' keywords in gitlab-ci.yml configuration files. !24098 (Paul Bonaud)
- Close More Actions tooltip when menu opens. !24285
- API: Support Jira transition ID as string. !24400 (Robert Schilling)
- Fixed navigation sidebar flashing open on page load. !24555
- Fix username escaping when using assign to me for issues. !24673
- commit page info-well overflow fix #56436. !24799 (Gokhan Apaydin)
- Fix error tracking list page. !24806
- Fix overlapping empty-header logo. !24868 (Jonas L.)
- Resolve Jobs tab border top in pipeline's page is 1px off. !24878
- Require maintainer access to show pages domain settings. !24926
- Display error message when API call to list Sentry issues fails. !24936
- Fix rollout status for statefulsets and daemonsets. !24972 (Sergej Nikolaev <kinolaev@gmail.com>)
- Display job names consistently on pipelines and environments list. !24984
- Update new password breadcrumb. !25037 (George Tsiolis)
- Fixes functions finder for upgraded Knative app. !25067
- Provide expires_in in LFS authentication payload. !25082
- Fix validation of certain ed25519 keys. !25115 (Merlijn B. W. Wajer)
- Timer and action name aligned vertically for delayed jobs in pipeline actions. !25117 (Gokhan Apaydin)
- Fix the border style of CONTRIBUTING button when it exists. !25124 (Takuya Noguchi)
- Change badges.svg example to pipeline.svg. !25157 (Aviad Levy)
- API: Fix docs and parameters for hangouts-chat service. !25180 (Robert Schilling)
- API: Expose full commit title. !25189 (Robert Schilling)
- API: Require only one parameter when updating a wiki. !25191 (Robert Schilling)
- Hide pipeline status when pipelines are disabled on project. !25204
- Fix alignment of dropdown icon on issuable on mobile. !25205 (Takuya Noguchi)
- Add left margin to 1st time contributor badge. !25216 (Gokhan Apaydin)
- Use limited counter for runner build count in admin page. !25220
- API: Ensure that related merge requests are referenced cross-project. !25222 (Robert Schilling)
- Ensure the base pipeline of a Merge Request belongs to its target branch. !25226
- Fix import_jid error on project import. !25239
- Fix commenting on commits having SHA1 starting with a large number. !25278
- Allow empty values such as [] to be stored in reactive cache. !25283
- Remove vertical connecting line placeholder from diff discussion notes. !25292
- Fix hover and active state colors of award emoji button. !25295
- Fix author layouts in issuable meta line UIs on mobile. !25332 (Takuya Noguchi)
- Fix bug where project topics truncate. !25398
- Fix ETag caching not being used for AJAX requests. !25400
- Doc - fix the url of pipeline status badge. !25404 (Aviad Levy)
- Fix pipeline status icon mismatch. !25407
- Allow users to compare branches on a read-only instance. !25414
- Fix 404s when C++ .gitignore template selected. !25416
- Always fetch MR latest version when creating suggestions. !25441
- Only show borders for markdown images in notes. !25448
- Bring back Rugged implementation of find_commit. !25477
- Remove duplicate units from metrics graph. !25485
- Fix project import error importing releases. !25495
- Remove duplicate XHR request when requesting new pipeline page. !25506
- Properly handle multiple X-Forwarded-For addresses in runner IP. !25511
- Fix weekday shift in issue board cards for UTC+X timezones by removing local timezone to UTC conversion. !25512 (Elias Werberich)
- Fix large table horizontal scroll and prevent side-by-side tables. !25520 (Dany Jupille)
- Fix error when viewing group issue boards when user doesn't have explicit group permissions. !25524
- Respect the should_remove_source_branch parameter to the merge API. !25525
- Externalize markdown toolbar buttons tooltips. !25529
- Fix method to mark a project repository as writable. !25546
- fix group without owner after transfer. !25573 (Peter Marko)
- Fix pagination and duplicate requests in environments page. !25582
- Improve the JS pagination to handle the case when the `X-Total` and `X-Total-Pages` headers aren't present. !25601
- Add right padding to the repository mirror action buttons. !25606
- Use 'folder-open' from sprite icons for Browse Files button in Tag page. !25635
- Make merge to refs/merge-requests/:iid/merge not raise when FF-only enabled. !25653
- Fixed "Copying comment with ordered list includes extraneous newlines". !25695
- Fix bridge jobs only/except variables policy. !25710
- Allow GraphQL requests without CSRF token. !25719
- Skip Project validation during Hashed Storage migration or rollback. !25753
- Resolve showing squash commit edit issue when only single commit is present. !25807
- Fix the last-ditch memory killer pgroup SIGKILL. !25940
- Disable timeout on merge request merging poll. !25988
- Allow modifying squash commit message for fast-forward only merge method. !26017
- Fix bug in BitBucket imports with SHA shorter than 40 chars. !26050
- Fix health checks not working behind load balancers. !26055
- Fix 500 error caused by CODEOWNERS with no matches. !26072
- Fix notes being marked as edited after resolving. !26143
- Fix error creating a merge request when diff includes a null byte. !26190
- Fix undefined variable error on json project views. !26297
- GitHub import: Create new branches as project owner. !26335
- Gracefully handles excluded fields from attributes during serialization on JsonCache. !26368
- Admin section finds users case-insensitively.
- Fixes not working dropdowns in pipelines page.
- Do not show file templates when creating a new directory in WebIDE.
- Allow project members to see private group if the project is in the group namespace.
- Allow maintainers to remove pages.
- Fix inconsistent pagination styles.
- Fixed blob editor deleting file content for certain file paths.
- Fix upcoming milestone when there are milestones with far-future due dates.
- Fixed alignment of changed icon in Web IDE.
### Changed (31 changes, 10 of them are from the community)
- Improve snippets empty state. !18348 (George Tsiolis)
- Remove second primary button on wiki edit. !19959 (George Tsiolis)
- Allow raw `tls_options` to be passed in LDAP configuration. !20678
- Remove undigested token column from personal_access_tokens table from the database. !22743
- Update activity filter for issues. !23423 (George Tsiolis)
- Use auto-build-image for build job in Auto-DevOps.gitlab-ci.yml. !24279
- Error tracking configuration - add a Sentry project selection dropdown. !24701
- Move ChatOps to Core. !24780
- Implement new arguments `state`, `closed_before` and `closed_after` for `IssuesResolver` in GraphQL. !24910
- Validate kubernetes cluster CA certificate. !24990
- Review App Link to Changed Page if Only One Change Present. !25048
- Show pipeline ID, commit, and branch name on modal while stopping pipeline. !25059
- Improve empty state for starred projects. !25138
- Capture due date when importing milestones from Github. !25182 (dstanley)
- Add a spinner icon which is rendered using pure css. !25186
- Make emoji picker bigger. !25187 (Jacopo Beschi @jacopo-beschi)
- API: Sort tie breaker with id DESC. !25311 (Nermin Vehabovic)
- Add iOS-fastlane template for .gitlab-ci.yml. !25395
- Move language setting to preferences. !25427 (Fabian Schneider @fabsrc)
- Resolve Create Project Template for Netlify. !25453
- Sort labels alphabetically on issues and merge requests list. !25470
- Add Project template for .NET Core. !25486
- Update operations settings breadcrumb trail. !25539 (George Tsiolis)
- Add Project template for go-micro. !25553
- Jira: make issue links title compact. !25609 (Elan Ruusamäe @glensc)
- Project level filtering for JupyterHub. !25684 (Amit Rathi (amit1rrr))
- Clean up vendored templates. !25794
- Mask all TOKEN and PASSWORD CI variables. !25868
- Add project template for Android. !25870
- Add iOS project template. !25872
- Upgrade to Gitaly v1.26.0. !25890
### Performance (11 changes)
- Improve performance for diverging commit counts. !24287
- Optimize Redis usage in User::ActivityService. !25005
- Only load syntax highlight CSS of selected theme. !25232
- Improve label select rendering. !25281
- Enable persisted pipeline stages by default. !25347
- Speed up group issue search counts. !25411
- Load repository language from the database if detected before. !25518
- Remove N+1 query for tags in /admin/runners page. !25572
- Eliminate most N+1 queries loading UserController#calendar_activities. !25697
- Improve Web IDE launch performance. !25700
- Significantly reduce N+1 queries in /api/v4/todos endpoint. !25711
### Added (55 changes, 18 of them are from the community)
- Add a tag filter to the admin runners view. !19740 (Alexis Reigel)
- Add project fetch statistics. !23596 (Jacopo Beschi @jacopo-beschi)
- Hashed Storage rollback mechanism. !23955
- Allow to recursively expand includes. !24356
- Allow expanding a diff to display full file. !24406
- Support `only: changes:` on MR pipelines. !24490 (Hiroyuki Sato)
- Expose additional merge request pipeline variables. !24595 (Hiroyuki Sato)
- Add metadata about the GitLab server to GraphQL. !24636
- Support merge ref writing (without merging to target branch). !24692
- Add field mergeRequests for project in GraphQL. !24805
- API support for MR merge to temporary merge ref path. !24918
- Ability to filter confidential issues. !24960 (Robert Schilling)
- Allow creation of branches that match a wildcard protection, except directly through git. !24969
- Add related merge request count to api response. !24974
- Add realtime validation for user fullname and username on validation. !25017 (Ehsan Abdulqader @EhsanZ)
- Allow setting feature flags per GitLab group through the API. !25022
- Add API endpoint to get a commit's GPG signature. !25032
- Add support for FTP assets for releases. !25071 (Robert Schilling)
- Add Confirmation Modal to Rollback on Environment. !25110
- add title attribute to display file name. !25154 (Satoshi Nakamatsu @satoshicano)
- API: Expose text_color for project and group labels. !25172 (Robert Schilling)
- Added support for ingress hostnames. !25181 (walkafwalka)
- API: Promote project milestone to a group milestone. !25203 (Nermin Vehabovic)
- API: Expose if the current user can merge a MR. !25207 (Robert Schilling)
- add readme to changelogs directory. !25209 (@glensc)
- API: Indicate if label is a project label. !25219 (Robert Schilling)
- Expose refspecs and depth to runner. !25233
- Port System Header and Footer feature to Core. !25241
- Sort Environments by Last Updated. !25260
- Accept force option to overwrite branch on commit via API. !25286
- Add support for masking CI variables. !25293
- Add Link from Closed (moved) Issues to Moved Issue. !25300
- Next/previous navigation between files in MR review. !25355
- Add YouTrack integration service. !25361 (Yauhen Kotau @bessorion)
- Add ability to set path and name for project on fork using API. !25363
- Add project level config for merge pipelines. !25385
- Edit Knative domain after it has been deployed. !25386
- Add zoom and scroll to metrics dashboard. !25388
- Persist source sha and target sha for merge pipelines. !25417
- Add support for toggling discussion filter from notes section. !25426
- Resolve Move files in the Web IDE. !25431
- Show header and footer system messages in email. !25474
- Allow configuring POSTGRES_VERSION in Auto DevOps. !25500
- Add Saturday to Localization first day of the week. !25509 (Ahmad Haghighi)
- Extend the Gitlab API for deletion of job_artifacts of a single job. !25522 (rroger)
- Simplify CI/CD configuration on serverless projects. !25523
- Add button to start discussion from single comment. !25575
- sidekiq: terminate child processes at shutdown. !25669
- Expose merge request entity for pipelines. !25679
- Link to most recent MR from a branch. !25689
- Adds Auto DevOps build job for tags. !25718 (walkafwalka)
- Allow all snippets to be accessed by API. !25772
- Make file tree in merge requests resizable.
- Make the Web IDE the default editor.
- File uploads are deleted asynchronously when deleting a project or group.
### Other (28 changes, 6 of them are from the community)
- Improve GitHub and Gitea project import table UI. !24606
- Externalize strings from `/app/views/projects/commit`. !24668 (George Tsiolis)
- Correct non-standard unicode spaces to regular unicode. !24795 (Marcel Amirault)
- Provide a performance bar link to the Jaeger UI. !24902
- Remove BATCH_SIZE from WikiFileFinder. !24933
- Use export-import svgs from gitlab-svgs. !24954
- Fix N+1 query in Issues and MergeRequest API when issuable_metadata is present. !25042 (Alex Koval)
- Directly inheriting from ActiveRecord::Migration is deprecated. !25066 (Jasper Maes)
- Bump Helm and kubectl in Auto DevOps to 2.12.3 and 1.11.7 respectively. !25072
- Log queue duration in production_json.log. !25075
- Extracted ResolveWithIssueButton to its own component. !25093 (Martin Hobert)
- Add rectangular project and group avatars. !25098
- Include note in the Rails filter_parameters configuration. !25238
- Bump Helm and kubectl used in Kubernetes integration to 2.12.3 and 1.11.7 respectively. !25268
- Include gl_project_path in API /internal/allowed response. !25314
- Fix incorrect Pages Domains checkbox description. !25392 (Anton Melser)
- Update GitLab Runner Helm Chart to 0.2.0. !25493
- Add suffix (`_event`) to merge request source. !25508
- Creates a helper function to check if repo is EE. !25647
- If chpst is available, make fron-source installations run sidekiq as a process group leader. !25654
- Bring back Rugged implementation of GetTreeEntries. !25674
- Moves EE util into the CE file. !25680
- Bring back Rugged implementation of CommitIsAncestor. !25702
- Bring back Rugged implementation of TreeEntry. !25706
- Enable syntax highlighting to other supported markups. !25761
- Update GitLab Shell to v8.7.1. !25801
- Bring back Rugged implementation of commit_tree_entry. !25896
- Removes EE differences for jobs/getters.js.
- Unreleased due to QA failure.
## 11.8.3 (2019-03-19) ## 11.8.3 (2019-03-19)
@ -82,33 +646,6 @@ entry.
- Allow project members to see private group if the project is in the group namespace. - Allow project members to see private group if the project is in the group namespace.
## 11.8.1 (2019-02-28)
### Security (21 changes)
- Stop linking to unrecognized package sources. !55518
- Don't allow non-members to see private related MRs.
- Do not display impersonated sessions under active sessions and remove ability to revoke session.
- Display only information visible to current user on the Milestone page.
- Show only merge requests visible to user on milestone detail page.
- Disable issue boards API when issues are disabled.
- Don't show new issue link after move when a user does not have permissions.
- Fix git clone revealing private repo's presence.
- Fix blind SSRF in Prometheus integration by checking URL before querying.
- Check snippet attached file to be moved is within designated directory.
- Check if desired milestone for an issue is available.
- Fix arbitrary file read via diffs during import.
- Display the correct number of MRs a user has access to.
- Forbid creating discussions for users with restricted access.
- Do not disclose milestone titles for unauthorized users.
- Validate session key when authorizing with GCP to create a cluster.
- Block local URLs for Kubernetes integration.
- Limit mermaid rendering to 5K characters.
- Remove the possibility to share a project with a group that a user is not a member of.
- Fix leaking private repository information in API.
- Prevent releases links API to leak tag existance.
## 11.8.0 (2019-02-22) ## 11.8.0 (2019-02-22)
### Security (7 changes, 1 of them is from the community) ### Security (7 changes, 1 of them is from the community)
@ -356,6 +893,40 @@ entry.
- Creates mixin to reduce code duplication between CE and EE in graph component. - Creates mixin to reduce code duplication between CE and EE in graph component.
## 11.7.10 (2019-03-28)
### Security (7 changes)
- Disallow guest users from accessing Releases.
- Fix PDF.js vulnerability.
- Hide "related branches" when user does not have permission.
- Fix XSS in resolve conflicts form.
- Added rake task for removing EXIF data from existing uploads.
- Disallow updating namespace when updating a project.
- Use UntrustedRegexp for matching refs policy.
## 11.7.8 (2019-03-26)
### Security (7 changes)
- Disallow guest users from accessing Releases.
- Fix PDF.js vulnerability.
- Hide "related branches" when user does not have permission.
- Fix XSS in resolve conflicts form.
- Added rake task for removing EXIF data from existing uploads.
- Disallow updating namespace when updating a project.
- Use UntrustedRegexp for matching refs policy.
## 11.7.7 (2019-03-19)
### Security (2 changes)
- Remove project serialization in quick actions response.
- Fixed ability to see private groups by users not belonging to given group.
## 11.7.5 (2019-02-06) ## 11.7.5 (2019-02-06)
### Fixed (8 changes) ### Fixed (8 changes)
@ -593,6 +1164,33 @@ entry.
- Update url placeholder for the sentry configuration page. !24338 - Update url placeholder for the sentry configuration page. !24338
## 11.6.10 (2019-02-28)
### Security (21 changes)
- Stop linking to unrecognized package sources. !55518
- Check snippet attached file to be moved is within designated directory.
- Fix potential Addressable::URI::InvalidURIError.
- Do not display impersonated sessions under active sessions and remove ability to revoke session.
- Display only information visible to current user on the Milestone page.
- Show only merge requests visible to user on milestone detail page.
- Disable issue boards API when issues are disabled.
- Don't show new issue link after move when a user does not have permissions.
- Fix git clone revealing private repo's presence.
- Fix blind SSRF in Prometheus integration by checking URL before querying.
- Check if desired milestone for an issue is available.
- Don't allow non-members to see private related MRs.
- Fix arbitrary file read via diffs during import.
- Display the correct number of MRs a user has access to.
- Forbid creating discussions for users with restricted access.
- Do not disclose milestone titles for unauthorized users.
- Validate session key when authorizing with GCP to create a cluster.
- Block local URLs for Kubernetes integration.
- Limit mermaid rendering to 5K characters.
- Remove the possibility to share a project with a group that a user is not a member of.
- Fix leaking private repository information in API.
## 11.6.8 (2019-01-30) ## 11.6.8 (2019-01-30)
- No changes. - No changes.

View file

@ -18,7 +18,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
## Contributing Documentation has been moved ## Contributing Documentation has been moved
As of July 2018, all the documentation for contributing to the GitLab project has been moved to a new location. As of July 2018, all the documentation for contributing to the GitLab project has been moved to a new location.
[view the new documentation](doc/development/contributing/index.md) to find the latest information. [View the new documentation](doc/development/contributing/index.md) to find the latest information.
## Contribute to GitLab ## Contribute to GitLab

View file

@ -1,4 +1,6 @@
danger.import_plugin('danger/plugins/helper.rb') danger.import_plugin('danger/plugins/helper.rb')
unless helper.release_automation?
danger.import_dangerfile(path: 'danger/metadata') danger.import_dangerfile(path: 'danger/metadata')
danger.import_dangerfile(path: 'danger/changes_size') danger.import_dangerfile(path: 'danger/changes_size')
danger.import_dangerfile(path: 'danger/changelog') danger.import_dangerfile(path: 'danger/changelog')
@ -11,3 +13,7 @@ danger.import_dangerfile(path: 'danger/commit_messages')
danger.import_dangerfile(path: 'danger/duplicate_yarn_dependencies') danger.import_dangerfile(path: 'danger/duplicate_yarn_dependencies')
danger.import_dangerfile(path: 'danger/prettier') danger.import_dangerfile(path: 'danger/prettier')
danger.import_dangerfile(path: 'danger/eslint') danger.import_dangerfile(path: 'danger/eslint')
danger.import_dangerfile(path: 'danger/roulette')
danger.import_dangerfile(path: 'danger/single_codebase')
danger.import_dangerfile(path: 'danger/gitlab_ui_wg')
end

View file

@ -1 +1 @@
1.20.1 1.34.1

View file

@ -1 +1 @@
8.4.4 9.0.0

View file

@ -1 +1 @@
8.3.3 8.5.2

50
Gemfile
View file

@ -18,7 +18,7 @@ gem 'gitlab-default_value_for', '~> 3.1.1', require: 'default_value_for'
gem 'mysql2', '~> 0.4.10', group: :mysql gem 'mysql2', '~> 0.4.10', group: :mysql
gem 'pg', '~> 1.1', group: :postgres gem 'pg', '~> 1.1', group: :postgres
gem 'rugged', '~> 0.27' gem 'rugged', '~> 0.28'
gem 'grape-path-helpers', '~> 1.0' gem 'grape-path-helpers', '~> 1.0'
gem 'faraday', '~> 0.12' gem 'faraday', '~> 0.12'
@ -42,11 +42,11 @@ gem 'omniauth-shibboleth', '~> 1.3.0'
gem 'omniauth-twitter', '~> 1.4' gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.3' gem 'omniauth-authentiq', '~> 0.3.3'
gem 'rack-oauth2', '~> 1.2.1' gem 'rack-oauth2', '~> 1.9.3'
gem 'jwt', '~> 2.1.0' gem 'jwt', '~> 2.1.0'
# Spam and anti-bot protection # Spam and anti-bot protection
gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails' gem 'recaptcha', '~> 4.11', require: 'recaptcha/rails'
gem 'akismet', '~> 2.0' gem 'akismet', '~> 2.0'
# Two-factor authentication # Two-factor authentication
@ -68,7 +68,7 @@ gem 'gpgme', '~> 2.0.18'
# LDAP Auth # LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes # GitLab fork with several improvements to original library. For full list of changes
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
gem 'gitlab_omniauth-ldap', '~> 2.0.4', require: 'omniauth-ldap' gem 'gitlab_omniauth-ldap', '~> 2.1.1', require: 'omniauth-ldap'
gem 'net-ldap' gem 'net-ldap'
# API # API
@ -94,13 +94,15 @@ gem 'carrierwave', '~> 1.3'
gem 'mini_magick' gem 'mini_magick'
# for backups # for backups
gem 'fog-aws', '~> 2.0.1' gem 'fog-aws', '~> 3.3'
gem 'fog-core', '~> 1.44' # Locked until fog-google resolves https://github.com/fog/fog-google/issues/421.
gem 'fog-google', '~> 1.7.1' # Also see config/initializers/fog_core_patch.rb.
gem 'fog-local', '~> 0.3' gem 'fog-core', '= 2.1.0'
gem 'fog-openstack', '~> 0.1' gem 'fog-google', '~> 1.8'
gem 'fog-local', '~> 0.6'
gem 'fog-openstack', '~> 1.0'
gem 'fog-rackspace', '~> 0.1.1' gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.2.0' gem 'fog-aliyun', '~> 0.3'
# for Google storage # for Google storage
gem 'google-api-client', '~> 0.23' gem 'google-api-client', '~> 0.23'
@ -114,7 +116,7 @@ gem 'seed-fu', '~> 2.3.7'
# Markdown and HTML processing # Markdown and HTML processing
gem 'html-pipeline', '~> 2.8' gem 'html-pipeline', '~> 2.8'
gem 'deckar01-task_list', '2.2.0' gem 'deckar01-task_list', '2.2.0'
gem 'gitlab-markup', '~> 1.6.5' gem 'gitlab-markup', '~> 1.7.0'
gem 'github-markup', '~> 1.7.0', require: 'github/markup' gem 'github-markup', '~> 1.7.0', require: 'github/markup'
gem 'commonmarker', '~> 0.17' gem 'commonmarker', '~> 0.17'
gem 'RedCloth', '~> 4.3.2' gem 'RedCloth', '~> 4.3.2'
@ -126,7 +128,7 @@ gem 'asciidoctor', '~> 1.5.8'
gem 'asciidoctor-plantuml', '0.0.8' gem 'asciidoctor-plantuml', '0.0.8'
gem 'rouge', '~> 3.1' gem 'rouge', '~> 3.1'
gem 'truncato', '~> 0.7.11' gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 2.7.0' gem 'bootstrap_form', '~> 4.2.0'
gem 'nokogiri', '~> 1.10.1' gem 'nokogiri', '~> 1.10.1'
gem 'escape_utils', '~> 1.1' gem 'escape_utils', '~> 1.1'
@ -137,13 +139,10 @@ gem 'icalendar'
gem 'diffy', '~> 3.1.0' gem 'diffy', '~> 3.1.0'
# Application server # Application server
# The 2.0.6 version of rack requires monkeypatch to be present in gem 'rack', '~> 2.0.7'
# `config.ru`. This can be removed once a new update for Rack
# is available that contains https://github.com/rack/rack/pull/1201.
gem 'rack', '2.0.6'
group :unicorn do group :unicorn do
gem 'unicorn', '~> 5.1.0' gem 'unicorn', '~> 5.4.1'
gem 'unicorn-worker-killer', '~> 0.4.4' gem 'unicorn-worker-killer', '~> 0.4.4'
end end
@ -156,7 +155,7 @@ end
gem 'state_machines-activerecord', '~> 0.5.1' gem 'state_machines-activerecord', '~> 0.5.1'
# Issue tags # Issue tags
gem 'acts-as-taggable-on', '~> 5.0' gem 'acts-as-taggable-on', '~> 6.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 5.2.1' gem 'sidekiq', '~> 5.2.1'
@ -168,7 +167,7 @@ gem 'gitlab-sidekiq-fetcher', '~> 0.4.0', require: 'sidekiq-reliable-fetch'
gem 'fugit', '~> 1.1' gem 'fugit', '~> 1.1'
# HTTP requests # HTTP requests
gem 'httparty', '~> 0.13.3' gem 'httparty', '~> 0.16.4'
# Colored output to console # Colored output to console
gem 'rainbow', '~> 3.0' gem 'rainbow', '~> 3.0'
@ -184,7 +183,7 @@ gem 're2', '~> 1.1.1'
# Misc # Misc
gem 'version_sorter', '~> 2.1.0' gem 'version_sorter', '~> 2.2.4'
# Export Ruby Regex to Javascript # Export Ruby Regex to Javascript
gem 'js_regex', '~> 3.1' gem 'js_regex', '~> 3.1'
@ -266,9 +265,7 @@ gem 'addressable', '~> 2.5.2'
gem 'font-awesome-rails', '~> 4.7' gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.3' gem 'gemojione', '~> 3.3'
gem 'gon', '~> 6.2' gem 'gon', '~> 6.2'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'request_store', '~> 1.3' gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1' gem 'virtus', '~> 1.0.1'
gem 'base32', '~> 0.3.0' gem 'base32', '~> 0.3.0'
@ -326,7 +323,7 @@ group :development do
end end
group :development, :test do group :development, :test do
gem 'bootsnap', '~> 1.3' gem 'bootsnap', '~> 1.4'
gem 'bullet', '~> 5.5.0', require: !!ENV['ENABLE_BULLET'] gem 'bullet', '~> 5.5.0', require: !!ENV['ENABLE_BULLET']
gem 'pry-byebug', '~> 3.5.1', platform: :mri gem 'pry-byebug', '~> 3.5.1', platform: :mri
gem 'pry-rails', '~> 0.3.4' gem 'pry-rails', '~> 0.3.4'
@ -383,7 +380,7 @@ group :test do
gem 'shoulda-matchers', '~> 3.1.2', require: false gem 'shoulda-matchers', '~> 3.1.2', require: false
gem 'email_spec', '~> 2.2.0' gem 'email_spec', '~> 2.2.0'
gem 'json-schema', '~> 2.8.0' gem 'json-schema', '~> 2.8.0'
gem 'webmock', '~> 2.3.2' gem 'webmock', '~> 3.5.1'
gem 'rails-controller-testing' gem 'rails-controller-testing'
gem 'sham_rack', '~> 1.3.6' gem 'sham_rack', '~> 1.3.6'
gem 'concurrent-ruby', '~> 1.1' gem 'concurrent-ruby', '~> 1.1'
@ -413,7 +410,7 @@ gem 'sys-filesystem', '~> 1.1.6'
# SSH host key support # SSH host key support
gem 'net-ssh', '~> 5.0' gem 'net-ssh', '~> 5.0'
gem 'sshkey', '~> 1.9.0' gem 'sshkey', '~> 2.0'
# Required for ED25519 SSH host key support # Required for ED25519 SSH host key support
group :ed25519 do group :ed25519 do
@ -422,7 +419,8 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 1.10.0', require: 'gitaly' gem 'gitaly-proto', '~> 1.19.0', require: 'gitaly'
gem 'grpc', '~> 1.15.0' gem 'grpc', '~> 1.15.0'
gem 'google-protobuf', '~> 3.6' gem 'google-protobuf', '~> 3.6'

View file

@ -43,8 +43,8 @@ GEM
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
tzinfo (~> 1.1) tzinfo (~> 1.1)
acts-as-taggable-on (5.0.0) acts-as-taggable-on (6.0.0)
activerecord (>= 4.2.8) activerecord (~> 5.0)
adamantium (0.2.0) adamantium (0.2.0)
ice_nine (~> 0.11.0) ice_nine (~> 0.11.0)
memoizable (~> 0.4.0) memoizable (~> 0.4.0)
@ -65,7 +65,7 @@ GEM
atomic (1.1.99) atomic (1.1.99)
attr_encrypted (3.1.0) attr_encrypted (3.1.0)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
attr_required (1.0.0) attr_required (1.0.1)
awesome_print (1.8.0) awesome_print (1.8.0)
axiom-types (0.1.1) axiom-types (0.1.1)
descendants_tracker (~> 0.0.4) descendants_tracker (~> 0.0.4)
@ -85,9 +85,11 @@ GEM
binding_ninja (0.2.2) binding_ninja (0.2.2)
binding_of_caller (0.8.0) binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.3.2) bootsnap (1.4.1)
msgpack (~> 1.0) msgpack (~> 1.0)
bootstrap_form (2.7.0) bootstrap_form (4.2.0)
actionpack (>= 5.0)
activemodel (>= 5.0)
brakeman (4.2.1) brakeman (4.2.1)
browser (2.5.3) browser (2.5.3)
builder (3.2.3) builder (3.2.3)
@ -218,32 +220,33 @@ GEM
flowdock (0.7.1) flowdock (0.7.1)
httparty (~> 0.7) httparty (~> 0.7)
multi_json multi_json
fog-aliyun (0.2.0) fog-aliyun (0.3.3)
fog-core (~> 1.27) fog-core
fog-json (~> 1.0) fog-json
ipaddress (~> 0.8) ipaddress (~> 0.8)
xml-simple (~> 1.1) xml-simple (~> 1.1)
fog-aws (2.0.1) fog-aws (3.3.0)
fog-core (~> 1.38) fog-core (~> 2.1)
fog-json (~> 1.0) fog-json (~> 1.1)
fog-xml (~> 0.1) fog-xml (~> 0.1)
ipaddress (~> 0.8) ipaddress (~> 0.8)
fog-core (1.45.0) fog-core (2.1.0)
builder builder
excon (~> 0.58) excon (~> 0.58)
formatador (~> 0.2) formatador (~> 0.2)
fog-google (1.7.1) mime-types
fog-core fog-google (1.8.2)
fog-json fog-core (<= 2.1.0)
fog-xml fog-json (~> 1.2)
fog-xml (~> 0.1.0)
google-api-client (~> 0.23.0) google-api-client (~> 0.23.0)
fog-json (1.0.2) fog-json (1.2.0)
fog-core (~> 1.0) fog-core
multi_json (~> 1.10) multi_json (~> 1.10)
fog-local (0.3.1) fog-local (0.6.0)
fog-core (~> 1.27) fog-core (>= 1.27, < 3.0)
fog-openstack (0.1.21) fog-openstack (1.0.8)
fog-core (>= 1.40) fog-core (~> 2.1)
fog-json (>= 1.0) fog-json (>= 1.0)
ipaddress (>= 0.8) ipaddress (>= 0.8)
fog-rackspace (0.1.1) fog-rackspace (0.1.1)
@ -259,7 +262,7 @@ GEM
foreman (0.84.0) foreman (0.84.0)
thor (~> 0.19.1) thor (~> 0.19.1)
formatador (0.2.5) formatador (0.2.5)
fugit (1.1.7) fugit (1.1.9)
et-orbi (~> 1.1, >= 1.1.7) et-orbi (~> 1.1, >= 1.1.7)
raabro (~> 1.1) raabro (~> 1.1)
fuubar (2.2.0) fuubar (2.2.0)
@ -278,19 +281,19 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (1.10.0) gitaly-proto (1.19.0)
grpc (~> 1.0) grpc (~> 1.0)
github-markup (1.7.0) github-markup (1.7.0)
gitlab-default_value_for (3.1.1) gitlab-default_value_for (3.1.1)
activerecord (>= 3.2.0, < 6.0) activerecord (>= 3.2.0, < 6.0)
gitlab-markup (1.6.5) gitlab-markup (1.7.0)
gitlab-sidekiq-fetcher (0.4.0) gitlab-sidekiq-fetcher (0.4.0)
sidekiq (~> 5) sidekiq (~> 5)
gitlab-styles (2.5.1) gitlab-styles (2.5.1)
rubocop (~> 0.54.0) rubocop (~> 0.54.0)
rubocop-gitlab-security (~> 0.1.0) rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19) rubocop-rspec (~> 1.19)
gitlab_omniauth-ldap (2.0.4) gitlab_omniauth-ldap (2.1.1)
net-ldap (~> 0.16) net-ldap (~> 0.16)
omniauth (~> 1.3) omniauth (~> 1.3)
pyu-ruby-sasl (>= 0.0.3.3, < 0.1) pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
@ -309,7 +312,7 @@ GEM
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
google-protobuf (3.6.1) google-protobuf (3.6.1)
googleapis-common-protos-types (1.0.2) googleapis-common-protos-types (1.0.3)
google-protobuf (~> 3.0) google-protobuf (~> 3.0)
googleauth (0.6.6) googleauth (0.6.6)
faraday (~> 0.12) faraday (~> 0.12)
@ -357,7 +360,7 @@ GEM
thor thor
tilt tilt
hangouts-chat (0.0.5) hangouts-chat (0.0.5)
hashdiff (0.3.4) hashdiff (0.3.8)
hashie (3.5.7) hashie (3.5.7)
hashie-forbidden_attributes (0.1.1) hashie-forbidden_attributes (0.1.1)
hashie (>= 3.0) hashie (>= 3.0)
@ -381,8 +384,8 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.1.1) http-form_data (2.1.1)
http_parser.rb (0.6.0) http_parser.rb (0.6.0)
httparty (0.13.7) httparty (0.16.4)
json (~> 1.8) mime-types (~> 3.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.6.0) i18n (1.6.0)
@ -400,7 +403,6 @@ GEM
activesupport activesupport
multipart-post multipart-post
oauth (~> 0.5, >= 0.5.0) oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2)
js_regex (3.1.1) js_regex (3.1.1)
character_set (~> 1.1) character_set (~> 1.1)
regexp_parser (~> 1.1) regexp_parser (~> 1.1)
@ -425,7 +427,7 @@ GEM
activerecord activerecord
kaminari-core (= 1.0.1) kaminari-core (= 1.0.1)
kaminari-core (1.0.1) kaminari-core (1.0.1)
kgio (2.10.0) kgio (2.11.2)
knapsack (1.17.0) knapsack (1.17.0)
rake rake
kubeclient (4.2.2) kubeclient (4.2.2)
@ -586,7 +588,7 @@ GEM
atomic (>= 1.0.0) atomic (>= 1.0.0)
peek peek
redis redis
pg (1.1.3) pg (1.1.4)
po_to_json (1.0.1) po_to_json (1.0.1)
json (>= 1.6.0) json (>= 1.6.0)
powerpack (0.1.1) powerpack (0.1.1)
@ -618,18 +620,18 @@ GEM
puma (>= 2.7, < 4) puma (>= 2.7, < 4)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
raabro (1.1.6) raabro (1.1.6)
rack (2.0.6) rack (2.0.7)
rack-accept (0.4.5) rack-accept (0.4.5)
rack (>= 0.4) rack (>= 0.4)
rack-attack (4.4.1) rack-attack (4.4.1)
rack rack
rack-cors (1.0.2) rack-cors (1.0.2)
rack-oauth2 (1.2.3) rack-oauth2 (1.9.3)
activesupport (>= 2.3) activesupport
attr_required (>= 0.0.5) attr_required
httpclient (>= 2.4) httpclient
multi_json (>= 1.3.6) json-jwt (>= 1.9.0)
rack (>= 1.1) rack
rack-protection (2.0.5) rack-protection (2.0.5)
rack rack
rack-proxy (0.6.0) rack-proxy (0.6.0)
@ -669,7 +671,7 @@ GEM
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (3.0.0) rainbow (3.0.0)
raindrops (0.18.0) raindrops (0.19.0)
rake (12.3.2) rake (12.3.2)
rb-fsevent (0.10.2) rb-fsevent (0.10.2)
rb-inotify (0.9.10) rb-inotify (0.9.10)
@ -682,7 +684,7 @@ GEM
optimist (>= 3.0.0) optimist (>= 3.0.0)
rdoc (6.0.4) rdoc (6.0.4)
re2 (1.1.1) re2 (1.1.1)
recaptcha (3.0.0) recaptcha (4.13.1)
json json
recursive-open-struct (1.1.0) recursive-open-struct (1.1.0)
redis (3.3.5) redis (3.3.5)
@ -787,7 +789,7 @@ GEM
rubyntlm (0.6.2) rubyntlm (0.6.2)
rubypants (0.2.0) rubypants (0.2.0)
rubyzip (1.2.2) rubyzip (1.2.2)
rugged (0.27.5) rugged (0.28.1)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (4.6.6) sanitize (4.6.6)
crass (~> 1.0.2) crass (~> 1.0.2)
@ -813,12 +815,10 @@ GEM
seed-fu (2.3.7) seed-fu (2.3.7)
activerecord (>= 3.1) activerecord (>= 3.1)
activesupport (>= 3.1) activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
selenium-webdriver (3.12.0) selenium-webdriver (3.12.0)
childprocess (~> 0.5) childprocess (~> 0.5)
rubyzip (~> 1.2) rubyzip (~> 1.2)
sentry-raven (2.7.4) sentry-raven (2.9.0)
faraday (>= 0.7.6, < 1.0) faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9) settingslogic (2.0.9)
sexp_processor (4.11.0) sexp_processor (4.11.0)
@ -858,7 +858,7 @@ GEM
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (1.3.13) sqlite3 (1.3.13)
sshkey (1.9.0) sshkey (2.0.0)
stackprof (0.2.10) stackprof (0.2.10)
state_machines (0.5.0) state_machines (0.5.0)
state_machines-activemodel (0.5.1) state_machines-activemodel (0.5.1)
@ -901,7 +901,7 @@ GEM
unf_ext unf_ext
unf_ext (0.0.7.5) unf_ext (0.0.7.5)
unicode-display_width (1.3.2) unicode-display_width (1.3.2)
unicorn (5.1.0) unicorn (5.4.1)
kgio (~> 2.6) kgio (~> 2.6)
raindrops (~> 0.7) raindrops (~> 0.7)
unicorn-worker-killer (0.4.4) unicorn-worker-killer (0.4.4)
@ -919,7 +919,7 @@ GEM
validates_hostname (1.0.6) validates_hostname (1.0.6)
activerecord (>= 3.0) activerecord (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
version_sorter (2.1.0) version_sorter (2.2.4)
virtus (1.0.5) virtus (1.0.5)
axiom-types (~> 0.1) axiom-types (~> 0.1)
coercible (~> 1.0) coercible (~> 1.0)
@ -928,7 +928,7 @@ GEM
vmstat (2.3.0) vmstat (2.3.0)
warden (1.2.7) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
webmock (2.3.2) webmock (3.5.1)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
@ -953,7 +953,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2) RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0) ace-rails-ap (~> 4.1.0)
activerecord_sane_schema_dumper (= 1.0) activerecord_sane_schema_dumper (= 1.0)
acts-as-taggable-on (~> 5.0) acts-as-taggable-on (~> 6.0)
addressable (~> 2.5.2) addressable (~> 2.5.2)
akismet (~> 2.0) akismet (~> 2.0)
asana (~> 0.8.1) asana (~> 0.8.1)
@ -968,8 +968,8 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0) benchmark-ips (~> 2.3.0)
better_errors (~> 2.5.0) better_errors (~> 2.5.0)
binding_of_caller (~> 0.8.0) binding_of_caller (~> 0.8.0)
bootsnap (~> 1.3) bootsnap (~> 1.4)
bootstrap_form (~> 2.7.0) bootstrap_form (~> 4.2.0)
brakeman (~> 4.2) brakeman (~> 4.2)
browser (~> 2.5) browser (~> 2.5)
bullet (~> 5.5.0) bullet (~> 5.5.0)
@ -1005,12 +1005,12 @@ DEPENDENCIES
flipper-active_record (~> 0.13.0) flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0) flipper-active_support_cache_store (~> 0.13.0)
flowdock (~> 0.7) flowdock (~> 0.7)
fog-aliyun (~> 0.2.0) fog-aliyun (~> 0.3)
fog-aws (~> 2.0.1) fog-aws (~> 3.3)
fog-core (~> 1.44) fog-core (= 2.1.0)
fog-google (~> 1.7.1) fog-google (~> 1.8)
fog-local (~> 0.3) fog-local (~> 0.6)
fog-openstack (~> 0.1) fog-openstack (~> 1.0)
fog-rackspace (~> 0.1.1) fog-rackspace (~> 0.1.1)
font-awesome-rails (~> 4.7) font-awesome-rails (~> 4.7)
foreman (~> 0.84.0) foreman (~> 0.84.0)
@ -1020,13 +1020,13 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 1.10.0) gitaly-proto (~> 1.19.0)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1) gitlab-default_value_for (~> 3.1.1)
gitlab-markup (~> 1.6.5) gitlab-markup (~> 1.7.0)
gitlab-sidekiq-fetcher (~> 0.4.0) gitlab-sidekiq-fetcher (~> 0.4.0)
gitlab-styles (~> 2.4) gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.2) gon (~> 6.2)
google-api-client (~> 0.23) google-api-client (~> 0.23)
google-protobuf (~> 3.6) google-protobuf (~> 3.6)
@ -1046,12 +1046,11 @@ DEPENDENCIES
hipchat (~> 1.5.0) hipchat (~> 1.5.0)
html-pipeline (~> 2.8) html-pipeline (~> 2.8)
html2text html2text
httparty (~> 0.13.3) httparty (~> 0.16.4)
icalendar icalendar
influxdb (~> 0.2) influxdb (~> 0.2)
jaeger-client (~> 0.10.0) jaeger-client (~> 0.10.0)
jira-ruby (~> 1.4) jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2)
js_regex (~> 3.1) js_regex (~> 3.1)
json-schema (~> 2.8.0) json-schema (~> 2.8.0)
jwt (~> 2.1.0) jwt (~> 2.1.0)
@ -1105,10 +1104,10 @@ DEPENDENCIES
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
puma (~> 3.12) puma (~> 3.12)
puma_worker_killer puma_worker_killer
rack (= 2.0.6) rack (~> 2.0.7)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
rack-cors (~> 1.0.0) rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1) rack-oauth2 (~> 1.9.3)
rack-proxy (~> 0.6.0) rack-proxy (~> 0.6.0)
rails (= 5.0.7.2) rails (= 5.0.7.2)
rails-controller-testing rails-controller-testing
@ -1120,7 +1119,7 @@ DEPENDENCIES
rbtrace (~> 0.4) rbtrace (~> 0.4)
rdoc (~> 6.0) rdoc (~> 6.0)
re2 (~> 1.1.1) re2 (~> 1.1.1)
recaptcha (~> 3.0) recaptcha (~> 4.11)
redis (~> 3.2) redis (~> 3.2)
redis-namespace (~> 1.6.0) redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2) redis-rails (~> 5.0.2)
@ -1141,13 +1140,12 @@ DEPENDENCIES
ruby-progressbar ruby-progressbar
ruby_parser (~> 3.8) ruby_parser (~> 3.8)
rubyzip (~> 1.2.2) rubyzip (~> 1.2.2)
rugged (~> 0.27) rugged (~> 0.28)
sanitize (~> 4.6) sanitize (~> 4.6)
sass (~> 3.5) sass (~> 3.5)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0) scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7) seed-fu (~> 2.3.7)
select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.12) selenium-webdriver (~> 3.12)
sentry-raven (~> 2.7) sentry-raven (~> 2.7)
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
@ -1161,7 +1159,7 @@ DEPENDENCIES
spring (~> 2.0.0) spring (~> 2.0.0)
spring-commands-rspec (~> 1.0.4) spring-commands-rspec (~> 1.0.4)
sprockets (~> 3.7.0) sprockets (~> 3.7.0)
sshkey (~> 1.9.0) sshkey (~> 2.0)
stackprof (~> 0.2.10) stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.5.1) state_machines-activerecord (~> 0.5.1)
sys-filesystem (~> 1.1.6) sys-filesystem (~> 1.1.6)
@ -1173,13 +1171,13 @@ DEPENDENCIES
u2f (~> 0.2.1) u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
unf (~> 0.1.4) unf (~> 0.1.4)
unicorn (~> 5.1.0) unicorn (~> 5.4.1)
unicorn-worker-killer (~> 0.4.4) unicorn-worker-killer (~> 0.4.4)
validates_hostname (~> 1.0.6) validates_hostname (~> 1.0.6)
version_sorter (~> 2.1.0) version_sorter (~> 2.2.4)
virtus (~> 1.0.1) virtus (~> 1.0.1)
vmstat (~> 2.3.0) vmstat (~> 2.3.0)
webmock (~> 2.3.2) webmock (~> 3.5.1)
webpack-rails (~> 0.9.10) webpack-rails (~> 0.9.10)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)

View file

@ -110,6 +110,18 @@ picked into the stable branches) up to the 19th of the month. Such merge
requests should have the ~"feature flag" label assigned, and don't require a requests should have the ~"feature flag" label assigned, and don't require a
corresponding exception request to be created. corresponding exception request to be created.
A level of common sense should be applied when deciding whether to have a feature
behind a feature flag off or on by default.
The following guideliness can be applied to help make this decision:
* If the feature is not fully ready or functioning, the feature flag should be disabled by default.
* If the feature is ready but there are concerns about performance or impact, the feature flag should be enabled by default, but
disabled via chatops before deployment on GitLab.com environments. If the performance concern is confirmed, the final release should have the feature flag disabled by default.
* In most other cases, the feature flag can be enabled by default.
For more information on rolling out changes using feature flags, read [through the documentation](https://docs.gitlab.com/ee/development/rolling_out_changes_using_feature_flags.html).
In order to build the final package and present the feature for self-hosted In order to build the final package and present the feature for self-hosted
customers, the feature flag should be removed. This should happen before the customers, the feature flag should be removed. This should happen before the
22nd, ideally _at least_ 2 days before. That means MRs with feature 22nd, ideally _at least_ 2 days before. That means MRs with feature
@ -156,8 +168,12 @@ on behalf of the community member.
Every new feature or change should be shipped with its corresponding documentation Every new feature or change should be shipped with its corresponding documentation
in accordance with the in accordance with the
[documentation process](https://docs.gitlab.com/ee/development/documentation/workflow.html) [documentation process](https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html)
and [structure](https://docs.gitlab.com/ee/development/documentation/structure.html). and [structure](https://docs.gitlab.com/ee/development/documentation/structure.html) guides.
Note that a technical writer will review all changes to documentation. This can occur
in the same MR as the feature code, but [if there is not sufficient time or need,
it can be planned via a follow-up issue for doc review](https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html#1-product-managers-role),
and another MR, if needed. Regardless, complete docs must be merged with code by the freeze.
#### What happens if these deadlines are missed? #### What happens if these deadlines are missed?
@ -186,8 +202,6 @@ and to prevent any last minute surprises.
Merge requests should still be complete, following the [definition of done][done]. Merge requests should still be complete, following the [definition of done][done].
#### Feature merge requests
If a merge request is not ready, but the developers and Product Manager If a merge request is not ready, but the developers and Product Manager
responsible for the feature think it is essential that it is in the release, responsible for the feature think it is essential that it is in the release,
they can [ask for an exception](#asking-for-an-exception) in advance. This is they can [ask for an exception](#asking-for-an-exception) in advance. This is
@ -202,34 +216,17 @@ information, see
[Automatic CE->EE merge][automatic_ce_ee_merge] and [Automatic CE->EE merge][automatic_ce_ee_merge] and
[Guidelines for implementing Enterprise Edition features][ee_features]. [Guidelines for implementing Enterprise Edition features][ee_features].
#### Documentation merge requests
Documentation is part of the product and must be shipped with the feature.
The single exception for the feature freeze is documentation, and it can
be left to be **merged up to the 14th** if:
* There is a follow-up issue to add documentation.
* It is assigned to the developer writing documentation for this feature, and they
are aware of it.
* It is in the correct milestone, with the labels ~Documentation, ~Deliverable,
~missed-deliverable, and "pick into X.Y" applied.
* It must be reviewed and approved by a technical writer.
For more information read the process for
[documentation shipped late](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late).
### After the 7th ### After the 7th
Once the stable branch is frozen, the only MRs that can be cherry-picked into Once the stable branch is frozen, the only MRs that can be cherry-picked into
the stable branch are: the stable branch are:
* Fixes for [regressions](#regressions) where the affected version `xx.x` in `regression:xx.x` is the current release. See [Managing bugs](#managing-bugs) section. * Fixes for [regressions](#regressions) where the affected version `xx.x` in `regression:xx.x` is the current release. See [Managing bugs](#managing-bugs) section.
* Fixes for security issues * Fixes for security issues.
* Fixes or improvements to automated QA scenarios * Fixes or improvements to automated QA scenarios.
* [Documentation updates](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late) for changes in the same release * [Documentation improvements](https://docs.gitlab.com/ee/development/documentation/workflow.html) for feature changes made in the same release, though initial docs for these features should have already been merged by the freeze, as required.
* New or updated translations (as long as they do not touch application code) * New or updated translations (as long as they do not touch application code).
* Changes that are behind a feature flag and have the ~"feature flag" label * Changes that are behind a feature flag and have the ~"feature flag" label.
During the feature freeze all merge requests that are meant to go into the During the feature freeze all merge requests that are meant to go into the
upcoming release should have the correct milestone assigned _and_ the upcoming release should have the correct milestone assigned _and_ the

12
Pipfile Normal file
View file

@ -0,0 +1,12 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
docutils = "==0.13.1"
[requires]
python_version = "3.4"

30
Pipfile.lock generated Normal file
View file

@ -0,0 +1,30 @@
{
"_meta": {
"hash": {
"sha256": "ec82d5e7c10fd591aeebbc9b7b62d730f7fd70dc52e4e4818834891aa4194c73"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.4"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"docutils": {
"hashes": [
"sha256:718c0f5fb677be0f34b781e04241c4067cbd9327b66bdd8e763201130f5175be",
"sha256:cb3ebcb09242804f84bdbf0b26504077a054da6772c6f4d625f335cc53ebf94d",
"sha256:de454f1015958450b72641165c08afe7023cd7e3944396448f2fb1b0ccba9d77"
],
"index": "pypi",
"version": "==0.13.1"
}
},
"develop": {}
}

View file

@ -1 +1 @@
11.8.9 11.10.4

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

View file

@ -12,6 +12,7 @@ const Api = {
projectsPath: '/api/:version/projects.json', projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id', projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels', projectLabelsPath: '/:namespace_path/:project_path/labels',
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
@ -111,6 +112,22 @@ const Api = {
return axios.get(url); return axios.get(url);
}, },
/**
* Get all Merge Requests for a project, eventually filtering based on
* supplied parameters
* @param projectPath
* @param params
* @returns {Promise}
*/
projectMergeRequests(projectPath, params = {}) {
const url = Api.buildUrl(Api.projectMergeRequestsPath).replace(
':id',
encodeURIComponent(projectPath),
);
return axios.get(url, { params });
},
// Return Merge Request for project // Return Merge Request for project
projectMergeRequest(projectPath, mergeRequestId, params = {}) { projectMergeRequest(projectPath, mergeRequestId, params = {}) {
const url = Api.buildUrl(Api.projectMergeRequestPath) const url = Api.buildUrl(Api.projectMergeRequestPath)

View file

@ -1,11 +1,12 @@
import $ from 'jquery'; import $ from 'jquery';
export default function groupAvatar() { export default function initAvatarPicker() {
$('.js-choose-group-avatar-button').on('click', function onClickGroupAvatar() { $('.js-choose-avatar-button').on('click', function onClickAvatar() {
const form = $(this).closest('form'); const form = $(this).closest('form');
return form.find('.js-group-avatar-input').click(); return form.find('.js-avatar-input').click();
}); });
$('.js-group-avatar-input').on('change', function onChangeAvatarInput() {
$('.js-avatar-input').on('change', function onChangeAvatarInput() {
const form = $(this).closest('form'); const form = $(this).closest('form');
const filename = $(this) const filename = $(this)
.val() .val()

View file

@ -8,6 +8,7 @@ import { updateTooltipTitle } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils'; import { isInVueNoteablePage } from './lib/utils/dom_utils';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import bp from './breakpoints';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
@ -264,7 +265,10 @@ export class AwardsHandler {
const css = { const css = {
top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`, top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`,
}; };
if (position === 'right') { // for xs screen we position the element on center
if (bp.getBreakpointSize() === 'xs') {
css.left = '5%';
} else if (position === 'right') {
css.left = `${$addBtn.offset().left - $menu.outerWidth() + 20}px`; css.left = `${$addBtn.offset().left - $menu.outerWidth() + 20}px`;
$menu.addClass('is-aligned-right'); $menu.addClass('is-aligned-right');
} else { } else {

View file

@ -90,7 +90,7 @@ export default {
}, },
badgeImageUrlExample() { badgeImageUrlExample() {
const exampleUrl = const exampleUrl =
'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/badge.svg'; 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/pipeline.svg';
return sprintf(s__('Badges|e.g. %{exampleUrl}'), { return sprintf(s__('Badges|e.g. %{exampleUrl}'), {
exampleUrl, exampleUrl,
}); });

View file

@ -10,10 +10,10 @@ export class CopyAsGFM {
const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent); const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent);
if (isIOS) return; if (isIOS) return;
$(document).on('copy', '.md, .wiki', e => { $(document).on('copy', '.md', e => {
CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection);
}); });
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', e => { $(document).on('copy', 'pre.code.highlight, table.code td.line_content', e => {
CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection);
}); });
$(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM); $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM);
@ -99,7 +99,7 @@ export class CopyAsGFM {
} }
static transformGFMSelection(documentFragment) { static transformGFMSelection(documentFragment) {
const gfmElements = documentFragment.querySelectorAll('.md, .wiki'); const gfmElements = documentFragment.querySelectorAll('.md');
switch (gfmElements.length) { switch (gfmElements.length) {
case 0: { case 0: {
return documentFragment; return documentFragment;
@ -173,7 +173,9 @@ export class CopyAsGFM {
wrapEl.appendChild(node.cloneNode(true)); wrapEl.appendChild(node.cloneNode(true));
const doc = DOMParser.fromSchema(schema.default).parse(wrapEl); const doc = DOMParser.fromSchema(schema.default).parse(wrapEl);
const res = markdownSerializer.default.serialize(doc); const res = markdownSerializer.default.serialize(doc, {
tightLists: true,
});
return res; return res;
}) })
.catch(() => {}); .catch(() => {});

View file

@ -4,6 +4,7 @@ import renderMath from './render_math';
import renderMermaid from './render_mermaid'; import renderMermaid from './render_mermaid';
import highlightCurrentUser from './highlight_current_user'; import highlightCurrentUser from './highlight_current_user';
import initUserPopovers from '../../user_popovers'; import initUserPopovers from '../../user_popovers';
import initMRPopovers from '../../mr_popover';
// Render GitLab flavoured Markdown // Render GitLab flavoured Markdown
// //
@ -15,6 +16,7 @@ $.fn.renderGFM = function renderGFM() {
renderMermaid(this.find('.js-render-mermaid')); renderMermaid(this.find('.js-render-mermaid'));
highlightCurrentUser(this.find('.gfm-project_member').get()); highlightCurrentUser(this.find('.gfm-project_member').get());
initUserPopovers(this.find('.gfm-project_member').get()); initUserPopovers(this.find('.gfm-project_member').get());
initMRPopovers(this.find('.gfm-merge_request').get());
return this; return this;
}; };

View file

@ -37,7 +37,7 @@ export default class ShortcutsIssuable extends Shortcuts {
} }
// Sanity check: Make sure the selected text comes from a discussion : it can either contain a message... // Sanity check: Make sure the selected text comes from a discussion : it can either contain a message...
let foundMessage = !!documentFragment.querySelector('.md, .wiki'); let foundMessage = !!documentFragment.querySelector('.md');
// ... Or come from a message // ... Or come from a message
if (!foundMessage) { if (!foundMessage) {
@ -46,7 +46,7 @@ export default class ShortcutsIssuable extends Shortcuts {
let node = e; let node = e;
do { do {
// Text nodes don't define the `matches` method // Text nodes don't define the `matches` method
if (node.matches && node.matches('.md, .wiki')) { if (node.matches && node.matches('.md')) {
foundMessage = true; foundMessage = true;
} }
node = node.parentNode; node = node.parentNode;

View file

@ -13,7 +13,7 @@ export default () => {
if (editBlobForm.length) { if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot'); const urlRoot = editBlobForm.data('relativeUrlRoot');
const assetsPath = editBlobForm.data('assetsPrefix'); const assetsPath = editBlobForm.data('assetsPrefix');
const filePath = editBlobForm.data('blobFilename'); const filePath = `${editBlobForm.data('blobFilename')}`;
const currentAction = $('.js-file-title').data('currentAction'); const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id'); const projectId = editBlobForm.data('project-id');
const isMarkdown = editBlobForm.data('is-markdown'); const isMarkdown = editBlobForm.data('is-markdown');

View file

@ -54,7 +54,10 @@ export default Vue.extend({
return `${n__('%d issue', '%d issues', issuesSize)}`; return `${n__('%d issue', '%d issues', issuesSize)}`;
}, },
isNewIssueShown() { isNewIssueShown() {
return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed'); return (
this.list.type === 'backlog' ||
(!this.disabled && this.list.type !== 'closed' && this.list.type !== 'blank')
);
}, },
}, },
watch: { watch: {

View file

@ -1,4 +1,5 @@
<script> <script>
import _ from 'underscore';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
@ -92,6 +93,9 @@ export default {
const { referencePath, groupId } = this.issue; const { referencePath, groupId } = this.issue;
return !groupId ? referencePath.split('#')[0] : null; return !groupId ? referencePath.split('#')[0] : null;
}, },
orderedLabels() {
return _.sortBy(this.issue.labels, 'title');
},
}, },
methods: { methods: {
isIndexLessThanlimit(index) { isIndexLessThanlimit(index) {
@ -176,7 +180,7 @@ export default {
</div> </div>
<div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap"> <div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap">
<button <button
v-for="label in issue.labels" v-for="label in orderedLabels"
v-if="showLabel(label)" v-if="showLabel(label)"
:key="label.id" :key="label.id"
v-gl-tooltip v-gl-tooltip

View file

@ -53,7 +53,7 @@ export default {
} else if (timeDifference === -1) { } else if (timeDifference === -1) {
return __('Yesterday'); return __('Yesterday');
} else if (timeDifference > 0 && timeDifference < 7) { } else if (timeDifference > 0 && timeDifference < 7) {
return dateFormat(issueDueDate, 'dddd', true); return dateFormat(issueDueDate, 'dddd');
} }
return standardDateFormat; return standardDateFormat;

View file

@ -8,7 +8,11 @@ import boardsStore from '../stores/boards_store';
$(document) $(document)
.off('created.label') .off('created.label')
.on('created.label', (e, label) => { .on('created.label', (e, label, addNewList) => {
if (!addNewList) {
return;
}
boardsStore.new({ boardsStore.new({
title: label.title, title: label.title,
position: boardsStore.state.lists.length - 2, position: boardsStore.state.lists.length - 2,

View file

@ -58,6 +58,7 @@ export default () => {
state: boardsStore.state, state: boardsStore.state,
loading: true, loading: true,
boardsEndpoint: $boardApp.dataset.boardsEndpoint, boardsEndpoint: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint, listsEndpoint: $boardApp.dataset.listsEndpoint,
boardId: $boardApp.dataset.boardId, boardId: $boardApp.dataset.boardId,
disabled: parseBoolean($boardApp.dataset.disabled), disabled: parseBoolean($boardApp.dataset.disabled),
@ -75,6 +76,7 @@ export default () => {
created() { created() {
gl.boardService = new BoardService({ gl.boardService = new BoardService({
boardsEndpoint: this.boardsEndpoint, boardsEndpoint: this.boardsEndpoint,
recentBoardsEndpoint: this.recentBoardsEndpoint,
listsEndpoint: this.listsEndpoint, listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath, bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId, boardId: this.boardId,

View file

@ -119,7 +119,17 @@ class ListIssue {
} }
const projectPath = this.project ? this.project.path : ''; const projectPath = this.project ? this.project.path : '';
return Vue.http.patch(`${this.path}.json`, data); return Vue.http.patch(`${this.path}.json`, data).then(({ body = {} } = {}) => {
/**
* Since post implementation of Scoped labels, server can reject
* same key-ed labels. To keep the UI and server Model consistent,
* we're just assigning labels that server echo's back to us when we
* PATCH the said object.
*/
if (body) {
this.labels = body.labels;
}
});
} }
} }

View file

@ -2,12 +2,13 @@ import axios from '../../lib/utils/axios_utils';
import { mergeUrlParams } from '../../lib/utils/url_utility'; import { mergeUrlParams } from '../../lib/utils/url_utility';
export default class BoardService { export default class BoardService {
constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) { constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
this.boardsEndpoint = boardsEndpoint; this.boardsEndpoint = boardsEndpoint;
this.boardId = boardId; this.boardId = boardId;
this.listsEndpoint = listsEndpoint; this.listsEndpoint = listsEndpoint;
this.listsEndpointGenerate = `${listsEndpoint}/generate.json`; this.listsEndpointGenerate = `${listsEndpoint}/generate.json`;
this.bulkUpdatePath = bulkUpdatePath; this.bulkUpdatePath = bulkUpdatePath;
this.recentBoardsEndpoint = `${recentBoardsEndpoint}.json`;
} }
generateBoardsPath(id) { generateBoardsPath(id) {

View file

@ -14,6 +14,9 @@ const BreakpointInstance = {
return breakpoint; return breakpoint;
}, },
isDesktop() {
return ['lg', 'md'].includes(this.getBreakpointSize());
},
}; };
export default BreakpointInstance; export default BreakpointInstance;

View file

@ -40,6 +40,12 @@ export default class VariableList {
// converted. we need the value as a string. // converted. we need the value as a string.
default: $('.js-ci-variable-input-protected').attr('data-default'), default: $('.js-ci-variable-input-protected').attr('data-default'),
}, },
masked: {
selector: '.js-ci-variable-input-masked',
// use `attr` instead of `data` as we don't want the value to be
// converted. we need the value as a string.
default: $('.js-ci-variable-input-masked').attr('data-default'),
},
environment_scope: { environment_scope: {
// We can't use a `.js-` class here because // We can't use a `.js-` class here because
// gl_dropdown replaces the <input> and doesn't copy over the class // gl_dropdown replaces the <input> and doesn't copy over the class
@ -88,13 +94,16 @@ export default class VariableList {
} }
}); });
this.$container.on('input trigger-change', inputSelector, e => {
// Always make sure there is an empty last row // Always make sure there is an empty last row
this.$container.on('input trigger-change', inputSelector, () => {
const $lastRow = this.$container.find('.js-row').last(); const $lastRow = this.$container.find('.js-row').last();
if (this.checkIfRowTouched($lastRow)) { if (this.checkIfRowTouched($lastRow)) {
this.insertRow($lastRow); this.insertRow($lastRow);
} }
// If masked, validate value against regex
this.validateMaskability($(e.currentTarget).closest('.js-row'));
}); });
} }
@ -171,12 +180,33 @@ export default class VariableList {
checkIfRowTouched($row) { checkIfRowTouched($row) {
return Object.keys(this.inputMap).some(name => { return Object.keys(this.inputMap).some(name => {
// Row should not qualify as touched if only switches have been touched
if (['protected', 'masked'].includes(name)) return false;
const entry = this.inputMap[name]; const entry = this.inputMap[name];
const $el = $row.find(entry.selector); const $el = $row.find(entry.selector);
return $el.length && $el.val() !== entry.default; return $el.length && $el.val() !== entry.default;
}); });
} }
validateMaskability($row) {
const invalidInputClass = 'gl-field-error-outline';
const maskableRegex = /^\w{8,}$/; // Eight or more alphanumeric characters plus underscores
const variableValue = $row.find(this.inputMap.secret_value.selector).val();
const isValueMaskable = maskableRegex.test(variableValue) || variableValue === '';
const isMaskedChecked = $row.find(this.inputMap.masked.selector).val() === 'true';
// Show a validation error if the user wants to mask an unmaskable variable value
$row
.find(this.inputMap.secret_value.selector)
.toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
$row
.find('.js-secret-value-placeholder')
.toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
$row.find('.masking-validation-error').toggle(isMaskedChecked && !isValueMaskable);
}
toggleEnableRow(isEnabled = true) { toggleEnableRow(isEnabled = true) {
this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled); this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled);
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled); this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);

View file

@ -12,6 +12,8 @@ import {
REQUEST_FAILURE, REQUEST_FAILURE,
UPGRADE_REQUESTED, UPGRADE_REQUESTED,
UPGRADE_REQUEST_FAILURE, UPGRADE_REQUEST_FAILURE,
INGRESS,
INGRESS_DOMAIN_SUFFIX,
} from './constants'; } 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';
@ -36,6 +38,7 @@ export default class Clusters {
installRunnerPath, installRunnerPath,
installJupyterPath, installJupyterPath,
installKnativePath, installKnativePath,
updateKnativePath,
installPrometheusPath, installPrometheusPath,
managePrometheusPath, managePrometheusPath,
hasRbac, hasRbac,
@ -62,6 +65,7 @@ export default class Clusters {
installPrometheusEndpoint: installPrometheusPath, installPrometheusEndpoint: installPrometheusPath,
installJupyterEndpoint: installJupyterPath, installJupyterEndpoint: installJupyterPath,
installKnativeEndpoint: installKnativePath, installKnativeEndpoint: installKnativePath,
updateKnativeEndpoint: updateKnativePath,
}); });
this.installApplication = this.installApplication.bind(this); this.installApplication = this.installApplication.bind(this);
@ -74,6 +78,10 @@ export default class Clusters {
this.successApplicationContainer = document.querySelector('.js-cluster-application-notice'); this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
this.showTokenButton = document.querySelector('.js-show-cluster-token'); this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token');
this.ingressDomainHelpText = document.querySelector('.js-ingress-domain-help-text');
this.ingressDomainSnippet = this.ingressDomainHelpText.querySelector(
'.js-ingress-domain-snippet',
);
Clusters.initDismissableCallout(); Clusters.initDismissableCallout();
initSettingsPanels(); initSettingsPanels();
@ -119,8 +127,7 @@ export default class Clusters {
static initDismissableCallout() { static initDismissableCallout() {
const callout = document.querySelector('.js-cluster-security-warning'); const callout = document.querySelector('.js-cluster-security-warning');
PersistentUserCallout.factory(callout);
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
} }
addListeners() { addListeners() {
@ -129,6 +136,8 @@ export default class Clusters {
eventHub.$on('upgradeApplication', data => this.upgradeApplication(data)); eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId)); 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('setKnativeHostname', data => this.setKnativeHostname(data));
} }
removeListeners() { removeListeners() {
@ -137,6 +146,8 @@ export default class Clusters {
eventHub.$off('upgradeApplication', this.upgradeApplication); eventHub.$off('upgradeApplication', this.upgradeApplication);
eventHub.$off('upgradeFailed', this.upgradeFailed); eventHub.$off('upgradeFailed', this.upgradeFailed);
eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess); eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname');
} }
initPolling() { initPolling() {
@ -177,6 +188,10 @@ export default class Clusters {
this.checkForNewInstalls(prevApplicationMap, this.store.state.applications); this.checkForNewInstalls(prevApplicationMap, this.store.state.applications);
this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason); this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);
this.toggleIngressDomainHelpText(
prevApplicationMap[INGRESS],
this.store.state.applications[INGRESS],
);
} }
showToken() { showToken() {
@ -247,7 +262,7 @@ export default class Clusters {
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.service.installApplication(appId, data.params).catch(() => { return this.service.installApplication(appId, data.params).catch(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE); this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
this.store.updateAppProperty( this.store.updateAppProperty(
appId, appId,
@ -272,6 +287,29 @@ export default class Clusters {
this.store.updateAppProperty(appId, 'requestStatus', null); this.store.updateAppProperty(appId, 'requestStatus', null);
} }
toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) {
const { externalIp, status } = ingressNewState;
const helpTextHidden = status !== APPLICATION_STATUS.INSTALLED || !externalIp;
const domainSnippetText = `${externalIp}${INGRESS_DOMAIN_SUFFIX}`;
if (ingressPreviousState.status !== status) {
this.ingressDomainHelpText.classList.toggle('hide', helpTextHidden);
this.ingressDomainSnippet.textContent = domainSnippetText;
}
}
saveKnativeDomain(data) {
const appId = data.id;
this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING);
this.service.updateApplication(appId, data.params);
}
setKnativeHostname(data) {
const appId = data.id;
this.store.updateAppProperty(appId, 'isEditingHostName', true);
this.store.updateAppProperty(appId, 'hostname', data.hostname);
}
destroy() { destroy() {
this.destroyed = true; this.destroyed = true;

View file

@ -191,14 +191,7 @@ export default {
return this.status === APPLICATION_STATUS.UPDATE_ERRORED; return this.status === APPLICATION_STATUS.UPDATE_ERRORED;
}, },
upgradeFailureDescription() { upgradeFailureDescription() {
return sprintf( return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
s__(
'ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again.',
),
{
title: this.title,
},
);
}, },
upgradeSuccessDescription() { upgradeSuccessDescription() {
return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), { return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), {
@ -210,9 +203,9 @@ export default {
if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) { if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) {
label = s__('ClusterIntegration|Upgrade'); label = s__('ClusterIntegration|Upgrade');
} else if (this.isUpgrading) { } else if (this.isUpgrading) {
label = s__('ClusterIntegration|Upgrading'); label = s__('ClusterIntegration|Updating');
} else if (this.upgradeFailed) { } else if (this.upgradeFailed) {
label = s__('ClusterIntegration|Retry upgrade'); label = s__('ClusterIntegration|Retry update');
} }
return label; return label;
@ -224,6 +217,14 @@ export default {
(this.upgradeRequested && !this.upgradeSuccessful) (this.upgradeRequested && !this.upgradeSuccessful)
); );
}, },
shouldShowUpgradeDetails() {
// This method only returns true when;
// Upgrade was successful OR Upgrade failed
// AND new upgrade is unavailable AND version information is present.
return (
(this.upgradeSuccessful || this.upgradeFailed) && !this.upgradeAvailable && this.version
);
},
}, },
watch: { watch: {
status() { status() {
@ -303,7 +304,7 @@ export default {
</div> </div>
<div <div
v-if="(upgradeSuccessful || upgradeFailed) && !upgradeAvailable" v-if="shouldShowUpgradeDetails"
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 }}

View file

@ -1,6 +1,7 @@
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import helmInstallIllustration from '@gitlab/svgs/dist/illustrations/kubernetes-installation.svg'; import helmInstallIllustration from '@gitlab/svgs/dist/illustrations/kubernetes-installation.svg';
import { GlLoadingIcon } from '@gitlab/ui';
import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png'; import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png'; import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.png'; import helmLogo from 'images/cluster_app_logos/helm.png';
@ -15,11 +16,15 @@ import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue'; import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants'; import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '~/clusters/event_hub';
export default { export default {
components: { components: {
applicationRow, applicationRow,
clipboardButton, clipboardButton,
LoadingButton,
GlLoadingIcon,
}, },
props: { props: {
type: { type: {
@ -86,53 +91,26 @@ export default {
ingressInstalled() { ingressInstalled() {
return this.applications.ingress.status === APPLICATION_STATUS.INSTALLED; return this.applications.ingress.status === APPLICATION_STATUS.INSTALLED;
}, },
ingressExternalIp() { ingressExternalEndpoint() {
return this.applications.ingress.externalIp; return this.applications.ingress.externalIp || this.applications.ingress.externalHostname;
}, },
certManagerInstalled() { certManagerInstalled() {
return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED; return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED;
}, },
ingressDescription() { ingressDescription() {
const extraCostParagraph = sprintf( return sprintf(
_.escape( _.escape(
s__( s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources `ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{pricingLink}.`,
like a load balancer, which may incur additional costs depending on
the hosting provider your Kubernetes cluster is installed on. If you are using
Google Kubernetes Engine, you can %{pricingLink}.`,
), ),
), ),
{ {
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, pricingLink: `<strong><a href="https://cloud.google.com/compute/pricing#lb"
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`, ${_.escape(s__('ClusterIntegration|pricing'))}</a></strong>`,
}, },
false, false,
); );
const externalIpParagraph = sprintf(
_.escape(
s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
),
),
{
ingressHelpLink: `<a href="${this.ingressHelpPath}">
${_.escape(s__('ClusterIntegration|More information'))}
</a>`,
},
false,
);
return `
<p>
${extraCostParagraph}
</p>
<p class="settings-message append-bottom-0">
${externalIpParagraph}
</p>
`;
}, },
certManagerDescription() { certManagerDescription() {
return sprintf( return sprintf(
@ -173,16 +151,70 @@ export default {
jupyterHostname() { jupyterHostname() {
return this.applications.jupyter.hostname; return this.applications.jupyter.hostname;
}, },
knativeInstalled() { knative() {
return this.applications.knative.status === APPLICATION_STATUS.INSTALLED; return this.applications.knative;
},
knativeInstalled() {
return (
this.knative.status === APPLICATION_STATUS.INSTALLED ||
this.knativeUpgrading ||
this.knativeUpgradeFailed ||
this.knative.status === APPLICATION_STATUS.UPDATED
);
},
knativeUpgrading() {
return (
this.knative.status === APPLICATION_STATUS.UPDATING ||
this.knative.status === APPLICATION_STATUS.SCHEDULED
);
},
knativeUpgradeFailed() {
return this.knative.status === APPLICATION_STATUS.UPDATE_ERRORED;
},
knativeExternalEndpoint() {
return this.knative.externalIp || this.knative.externalHostname;
},
knativeDescription() {
return sprintf(
_.escape(
s__(
`ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}.`,
),
),
{
pricingLink: `<strong><a href="https://cloud.google.com/compute/pricing#lb"
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|pricing'))}</a></strong>`,
},
false,
);
},
canUpdateKnativeEndpoint() {
return this.knativeExternalEndpoint && !this.knativeUpgradeFailed && !this.knativeUpgrading;
},
knativeHostname: {
get() {
return this.knative.hostname;
},
set(hostname) {
eventHub.$emit('setKnativeHostname', {
id: 'knative',
hostname,
});
}, },
knativeExternalIp() {
return this.applications.knative.externalIp;
}, },
}, },
created() { created() {
this.helmInstallIllustration = helmInstallIllustration; this.helmInstallIllustration = helmInstallIllustration;
}, },
methods: {
saveKnativeDomain() {
eventHub.$emit('saveKnativeDomain', {
id: 'knative',
params: { hostname: this.knative.hostname },
});
},
},
}; };
</script> </script>
@ -247,30 +279,35 @@ export default {
<template v-if="ingressInstalled"> <template v-if="ingressInstalled">
<div class="form-group"> <div class="form-group">
<label for="ingress-ip-address"> <label for="ingress-endpoint">
{{ s__('ClusterIntegration|Ingress IP Address') }} {{ s__('ClusterIntegration|Ingress Endpoint') }}
</label> </label>
<div v-if="ingressExternalIp" class="input-group"> <div v-if="ingressExternalEndpoint" class="input-group">
<input <input
id="ingress-ip-address" id="ingress-endpoint"
:value="ingressExternalIp" :value="ingressExternalEndpoint"
type="text" type="text"
class="form-control js-ip-address" class="form-control js-endpoint"
readonly readonly
/> />
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="ingressExternalIp" :text="ingressExternalEndpoint"
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')" :title="s__('ClusterIntegration|Copy Ingress Endpoint to clipboard')"
class="input-group-text js-clipboard-btn" class="input-group-text js-clipboard-btn"
/> />
</span> </span>
</div> </div>
<input v-else type="text" class="form-control js-ip-address" readonly value="?" /> <div v-else class="input-group">
<input type="text" class="form-control js-endpoint" readonly />
<gl-loading-icon
class="position-absolute align-self-center ml-2 js-ingress-ip-loading-icon"
/>
</div>
<p class="form-text text-muted"> <p class="form-text text-muted">
{{ {{
s__(`ClusterIntegration|Point a wildcard DNS to this s__(`ClusterIntegration|Point a wildcard DNS to this
generated IP address in order to access generated endpoint in order to access
your application after it has been deployed.`) your application after it has been deployed.`)
}} }}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
@ -279,19 +316,21 @@ export default {
</p> </p>
</div> </div>
<p v-if="!ingressExternalIp" class="settings-message js-no-ip-message"> <p v-if="!ingressExternalEndpoint" class="settings-message js-no-endpoint-message">
{{ {{
s__(`ClusterIntegration|The IP address is in s__(`ClusterIntegration|The endpoint is in
the process of being assigned. Please check your Kubernetes the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}} }}
<a :href="ingressHelpPath" target="_blank" rel="noopener noreferrer"> <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }} {{ __('More information') }}
</a> </a>
</p> </p>
</template> </template>
<div v-html="ingressDescription"></div> <template v-if="!ingressInstalled">
<div class="bs-callout bs-callout-info" v-html="ingressDescription"></div>
</template>
</div> </div>
</application-row> </application-row>
<application-row <application-row
@ -354,7 +393,6 @@ export default {
<div slot="description" v-html="prometheusDescription"></div> <div slot="description" v-html="prometheusDescription"></div>
</application-row> </application-row>
<application-row <application-row
v-if="isProjectCluster"
id="runner" id="runner"
:logo-url="gitlabLogo" :logo-url="gitlabLogo"
:title="applications.runner.title" :title="applications.runner.title"
@ -370,9 +408,9 @@ export default {
> >
<div slot="description"> <div slot="description">
{{ {{
s__(`ClusterIntegration|GitLab Runner connects to this s__(`ClusterIntegration|GitLab Runner connects to the
project's repository and executes CI/CD jobs, repository and executes CI/CD jobs,
pushing results back and deploying, pushing results back and deploying
applications to production.`) applications to production.`)
}} }}
</div> </div>
@ -401,7 +439,7 @@ export default {
}} }}
</p> </p>
<template v-if="ingressExternalIp"> <template v-if="ingressExternalEndpoint">
<div class="form-group"> <div class="form-group">
<label for="jupyter-hostname"> <label for="jupyter-hostname">
{{ s__('ClusterIntegration|Jupyter Hostname') }} {{ s__('ClusterIntegration|Jupyter Hostname') }}
@ -451,7 +489,7 @@ export default {
> >
<div slot="description"> <div slot="description">
<span v-if="!rbac"> <span v-if="!rbac">
<p v-if="!rbac" class="bs-callout bs-callout-info append-bottom-0"> <p v-if="!rbac" class="rbac-notice bs-callout bs-callout-info append-bottom-0">
{{ {{
s__(`ClusterIntegration|You must have an RBAC-enabled cluster s__(`ClusterIntegration|You must have an RBAC-enabled cluster
to install Knative.`) to install Knative.`)
@ -471,77 +509,88 @@ export default {
}} }}
</p> </p>
<template v-if="knativeInstalled"> <div class="row">
<div class="form-group"> <template v-if="knativeInstalled || (helmInstalled && rbac)">
<div
:class="{ 'col-md-6': knativeInstalled, 'col-12': helmInstalled && rbac }"
class="form-group col-sm-12 mb-0"
>
<label for="knative-domainname"> <label for="knative-domainname">
<strong>
{{ s__('ClusterIntegration|Knative Domain Name:') }} {{ s__('ClusterIntegration|Knative Domain Name:') }}
</strong>
</label> </label>
<input <input
id="knative-domainname" id="knative-domainname"
v-model="applications.knative.hostname" v-model="knativeHostname"
type="text" type="text"
class="form-control js-domainname" class="form-control js-knative-domainname"
readonly
/>
</div>
</template>
<template v-else-if="helmInstalled && rbac">
<div class="form-group">
<label for="knative-domainname">
{{ s__('ClusterIntegration|Knative Domain Name:') }}
</label>
<input
id="knative-domainname"
v-model="applications.knative.hostname"
type="text"
class="form-control js-domainname"
/> />
</div> </div>
</template> </template>
<template v-if="knativeInstalled"> <template v-if="knativeInstalled">
<div class="form-group"> <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
<label for="knative-ip-address"> <label for="knative-endpoint">
{{ s__('ClusterIntegration|Knative IP Address:') }} <strong>
{{ s__('ClusterIntegration|Knative Endpoint:') }}
</strong>
</label> </label>
<div v-if="knativeExternalIp" class="input-group"> <div v-if="knativeExternalEndpoint" class="input-group">
<input <input
id="knative-ip-address" id="knative-endpoint"
:value="knativeExternalIp" :value="knativeExternalEndpoint"
type="text" type="text"
class="form-control js-ip-address" class="form-control js-knative-endpoint"
readonly readonly
/> />
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="knativeExternalIp" :text="knativeExternalEndpoint"
:title="s__('ClusterIntegration|Copy Knative IP Address to clipboard')" :title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')"
class="input-group-text js-clipboard-btn" class="input-group-text js-knative-endpoint-clipboard-btn"
/> />
</span> </span>
</div> </div>
<input v-else type="text" class="form-control js-ip-address" readonly value="?" /> <div v-else class="input-group">
<input type="text" class="form-control js-endpoint" readonly />
<gl-loading-icon
class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
/>
</div>
</div> </div>
<p v-if="!knativeExternalIp" class="settings-message js-no-ip-message"> <p class="form-text text-muted col-12">
{{ {{
s__(`ClusterIntegration|The IP address is in s__(
the process of being assigned. Please check your Kubernetes `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) )
}}
</p>
<p>
{{
s__(`ClusterIntegration|Point a wildcard DNS to this
generated IP address in order to access
your application after it has been deployed.`)
}} }}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }} {{ __('More information') }}
</a> </a>
</p> </p>
<p
v-if="!knativeExternalEndpoint"
class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
>
{{
s__(`ClusterIntegration|The endpoint is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
</p>
<button
v-if="canUpdateKnativeEndpoint"
class="btn btn-success js-knative-save-domain-button mt-3 ml-3"
@click="saveKnativeDomain"
>
{{ s__('ClusterIntegration|Save changes') }}
</button>
</template> </template>
</div> </div>
</div>
</application-row> </application-row>
</div> </div>
</section> </section>

View file

@ -28,3 +28,4 @@ export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative'; export const KNATIVE = 'knative';
export const RUNNER = 'runner'; export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager'; export const CERT_MANAGER = 'cert_manager';
export const INGRESS_DOMAIN_SUFFIX = '.nip.io';

View file

@ -12,6 +12,9 @@ export default class ClusterService {
jupyter: this.options.installJupyterEndpoint, jupyter: this.options.installJupyterEndpoint,
knative: this.options.installKnativeEndpoint, knative: this.options.installKnativeEndpoint,
}; };
this.appUpdateEndpointMap = {
knative: this.options.updateKnativeEndpoint,
};
} }
fetchData() { fetchData() {
@ -22,6 +25,10 @@ export default class ClusterService {
return axios.post(this.appInstallEndpointMap[appId], params); return axios.post(this.appInstallEndpointMap[appId], params);
} }
updateApplication(appId, params) {
return axios.patch(this.appUpdateEndpointMap[appId], params);
}
static updateCluster(endpoint, data) { static updateCluster(endpoint, data) {
return axios.put(endpoint, data); return axios.put(endpoint, data);
} }

View file

@ -25,6 +25,7 @@ export default class ClusterStore {
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
externalIp: null, externalIp: null,
externalHostname: null,
}, },
cert_manager: { cert_manager: {
title: s__('ClusterIntegration|Cert-Manager'), title: s__('ClusterIntegration|Cert-Manager'),
@ -66,7 +67,9 @@ export default class ClusterStore {
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
hostname: null, hostname: null,
isEditingHostName: false,
externalIp: null, externalIp: null,
externalHostname: null,
}, },
}, },
}; };
@ -119,6 +122,7 @@ export default class ClusterStore {
if (appId === INGRESS) { if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip; this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
this.state.applications.ingress.externalHostname = serverAppEntry.external_hostname;
} else if (appId === CERT_MANAGER) { } else if (appId === CERT_MANAGER) {
this.state.applications.cert_manager.email = this.state.applications.cert_manager.email =
this.state.applications.cert_manager.email || serverAppEntry.email; this.state.applications.cert_manager.email || serverAppEntry.email;
@ -129,10 +133,14 @@ export default class ClusterStore {
? `jupyter.${this.state.applications.ingress.externalIp}.nip.io` ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
: ''); : '');
} else if (appId === KNATIVE) { } else if (appId === KNATIVE) {
if (!this.state.applications.knative.isEditingHostName) {
this.state.applications.knative.hostname = this.state.applications.knative.hostname =
serverAppEntry.hostname || this.state.applications.knative.hostname; serverAppEntry.hostname || this.state.applications.knative.hostname;
}
this.state.applications.knative.externalIp = this.state.applications.knative.externalIp =
serverAppEntry.external_ip || this.state.applications.knative.externalIp; serverAppEntry.external_ip || this.state.applications.knative.externalIp;
this.state.applications.knative.externalHostname =
serverAppEntry.external_hostname || this.state.applications.knative.externalHostname;
} else if (appId === RUNNER) { } else if (appId === RUNNER) {
this.state.applications.runner.version = version; this.state.applications.runner.version = version;
this.state.applications.runner.upgradeAvailable = upgradeAvailable; this.state.applications.runner.upgradeAvailable = upgradeAvailable;

View file

@ -71,29 +71,39 @@ export default class ImageFile {
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
initDraggable($el, padding, callback) { initDraggable($el, padding, callback) {
var dragging = false; var dragging = false;
var $body = $('body'); const $body = $('body');
var $offsetEl = $el.parent(); const $offsetEl = $el.parent();
const dragStart = function() {
$el.off('mousedown').on('mousedown', function() {
dragging = true; dragging = true;
$body.css('user-select', 'none'); $body.css('user-select', 'none');
}); };
const dragStop = function() {
dragging = false;
$body.css('user-select', '');
};
const dragMove = function(e) {
const moveX = e.pageX || e.touches[0].pageX;
const left = moveX - ($offsetEl.offset().left + padding);
if (!dragging) return;
callback(e, left);
};
$el
.off('mousedown')
.off('touchstart')
.on('mousedown', dragStart)
.on('touchstart', dragStart);
$body $body
.off('mouseup') .off('mouseup')
.off('mousemove') .off('mousemove')
.on('mouseup', function() { .off('touchend')
dragging = false; .off('touchmove')
$body.css('user-select', ''); .on('mouseup', dragStop)
}) .on('touchend', dragStop)
.on('mousemove', function(e) { .on('mousemove', dragMove)
var left; .on('touchmove', dragMove);
if (!dragging) return;
left = e.pageX - ($offsetEl.offset().left + padding);
callback(e, left);
});
} }
prepareFrames(view) { prepareFrames(view) {

View file

@ -16,3 +16,63 @@ $.fn.extend({
.removeClass('disabled'); .removeClass('disabled');
}, },
}); });
/*
Starting with bootstrap 4.3.1, bootstrap sanitizes html used for tooltips / popovers.
This extends the default whitelists with more elements / attributes:
https://getbootstrap.com/docs/4.3/getting-started/javascript/#sanitizer
*/
const whitelist = $.fn.tooltip.Constructor.Default.whiteList;
const inputAttributes = ['value', 'type'];
const dataAttributes = [
'data-toggle',
'data-placement',
'data-container',
'data-title',
'data-class',
'data-clipboard-text',
'data-placement',
];
// Whitelisting data attributes
whitelist['*'] = [
...whitelist['*'],
...dataAttributes,
'title',
'width height',
'abbr',
'datetime',
'name',
'width',
'height',
];
// Whitelist missing elements:
whitelist.label = ['for'];
whitelist.button = [...inputAttributes];
whitelist.input = [...inputAttributes];
whitelist.tt = [];
whitelist.samp = [];
whitelist.kbd = [];
whitelist.var = [];
whitelist.dfn = [];
whitelist.cite = [];
whitelist.big = [];
whitelist.address = [];
whitelist.dl = [];
whitelist.dt = [];
whitelist.dd = [];
whitelist.abbr = [];
whitelist.acronym = [];
whitelist.blockquote = [];
whitelist.del = [];
whitelist.ins = [];
whitelist['gl-emoji'] = [];
// Whitelisting SVG tags and attributes
whitelist.svg = ['viewBox'];
whitelist.use = ['xlink:href'];
whitelist.path = ['d'];

View file

@ -3,7 +3,7 @@ import 'jquery';
// common jQuery plugins // common jQuery plugins
import 'jquery-ujs'; import 'jquery-ujs';
import 'vendor/jquery.endless-scroll'; import 'vendor/jquery.endless-scroll';
import 'vendor/jquery.caret'; import 'jquery.caret'; // must be imported before at.js
import 'vendor/jquery.atwho'; import 'at.js';
import 'vendor/jquery.scrollTo'; import 'vendor/jquery.scrollTo';
import 'jquery.waitforimages'; import 'jquery.waitforimages';

View file

@ -7,6 +7,7 @@ import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign'; import 'core-js/fn/object/assign';
import 'core-js/fn/object/values'; import 'core-js/fn/object/values';
import 'core-js/fn/promise'; import 'core-js/fn/promise';
import 'core-js/fn/promise/finally';
import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point'; import 'core-js/fn/string/from-code-point';
import 'core-js/fn/string/includes'; import 'core-js/fn/string/includes';

View file

@ -4,6 +4,10 @@ import _ from 'underscore';
import bp from './breakpoints'; import bp from './breakpoints';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
// NOTE: at 1200px nav sidebar should not overlap the content
// https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24555#note_134136110
const NAV_SIDEBAR_BREAKPOINT = 1200;
export default class ContextualSidebar { export default class ContextualSidebar {
constructor() { constructor() {
this.initDomElements(); this.initDomElements();
@ -26,44 +30,57 @@ export default class ContextualSidebar {
bindEvents() { bindEvents() {
if (!this.$sidebar.length) return; if (!this.$sidebar.length) return;
document.addEventListener('click', e => {
if (
!e.target.closest('.nav-sidebar') &&
(bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')
) {
this.toggleCollapsedSidebar(true, true);
}
});
this.$openSidebar.on('click', () => this.toggleSidebarNav(true)); this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
this.$closeSidebar.on('click', () => this.toggleSidebarNav(false)); this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
this.$overlay.on('click', () => this.toggleSidebarNav(false)); this.$overlay.on('click', () => this.toggleSidebarNav(false));
this.$sidebarToggle.on('click', () => { this.$sidebarToggle.on('click', () => {
if (!ContextualSidebar.isDesktopBreakpoint()) {
this.toggleSidebarNav(!this.$sidebar.hasClass('sidebar-expanded-mobile'));
} else {
const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop'); const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop');
this.toggleCollapsedSidebar(value, true); this.toggleCollapsedSidebar(value, true);
}
});
this.$page.on('transitionstart transitionend', () => {
$(document).trigger('content.resize');
}); });
$(window).on('resize', () => _.debounce(this.render(), 100)); $(window).on('resize', () => _.debounce(this.render(), 100));
} }
// TODO: use the breakpoints from breakpoints.js once they have been updated for bootstrap 4
// See documentation: https://design.gitlab.com/regions/navigation#contextual-navigation
static isDesktopBreakpoint = () => bp.windowWidth() >= NAV_SIDEBAR_BREAKPOINT;
static setCollapsedCookie(value) { static setCollapsedCookie(value) {
if (bp.getBreakpointSize() !== 'lg') { if (!ContextualSidebar.isDesktopBreakpoint()) {
return; return;
} }
Cookies.set('sidebar_collapsed', value, { expires: 365 * 10 }); Cookies.set('sidebar_collapsed', value, { expires: 365 * 10 });
} }
toggleSidebarNav(show) { toggleSidebarNav(show) {
this.$sidebar.toggleClass('sidebar-expanded-mobile', show); const breakpoint = bp.getBreakpointSize();
this.$overlay.toggleClass('mobile-nav-open', show); const dbp = ContextualSidebar.isDesktopBreakpoint();
this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? show : false);
this.$overlay.toggleClass(
'mobile-nav-open',
breakpoint === 'xs' || breakpoint === 'sm' ? show : false,
);
this.$sidebar.removeClass('sidebar-collapsed-desktop'); this.$sidebar.removeClass('sidebar-collapsed-desktop');
} }
toggleCollapsedSidebar(collapsed, saveCookie) { toggleCollapsedSidebar(collapsed, saveCookie) {
const breakpoint = bp.getBreakpointSize(); const breakpoint = bp.getBreakpointSize();
const dbp = ContextualSidebar.isDesktopBreakpoint();
if (this.$sidebar.length) { if (this.$sidebar.length) {
this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed); this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed);
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? !collapsed : false);
this.$page.toggleClass(
'page-with-icon-sidebar',
breakpoint === 'xs' || breakpoint === 'sm' ? true : collapsed,
);
} }
if (saveCookie) { if (saveCookie) {
@ -84,13 +101,11 @@ export default class ContextualSidebar {
render() { render() {
if (!this.$sidebar.length) return; if (!this.$sidebar.length) return;
const breakpoint = bp.getBreakpointSize(); if (!ContextualSidebar.isDesktopBreakpoint()) {
this.toggleSidebarNav(false);
if (breakpoint === 'sm' || breakpoint === 'md') { } else {
this.toggleCollapsedSidebar(true, false);
} else if (breakpoint === 'lg') {
const collapse = parseBoolean(Cookies.get('sidebar_collapsed')); const collapse = parseBoolean(Cookies.get('sidebar_collapsed'));
this.toggleCollapsedSidebar(collapse, false); this.toggleCollapsedSidebar(collapse, true);
} }
} }
} }

View file

@ -14,6 +14,7 @@ export default class CreateLabelDropdown {
this.$newLabelField = $('#new_label_name', this.$el); this.$newLabelField = $('#new_label_name', this.$el);
this.$newColorField = $('#new_label_color', this.$el); this.$newColorField = $('#new_label_color', this.$el);
this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el); this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
this.$addList = $('.js-add-list', this.$el);
this.$newLabelError = $('.js-label-error', this.$el); this.$newLabelError = $('.js-label-error', this.$el);
this.$newLabelCreateButton = $('.js-new-label-btn', this.$el); this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el); this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
@ -21,6 +22,8 @@ export default class CreateLabelDropdown {
this.$newLabelError.hide(); this.$newLabelError.hide();
this.$newLabelCreateButton.disable(); this.$newLabelCreateButton.disable();
this.addListDefault = this.$addList.is(':checked');
this.cleanBinding(); this.cleanBinding();
this.addBinding(); this.addBinding();
} }
@ -83,6 +86,8 @@ export default class CreateLabelDropdown {
this.$newColorField.val('').trigger('change'); this.$newColorField.val('').trigger('change');
this.$addList.prop('checked', this.addListDefault);
this.$colorPreview this.$colorPreview
.css('background-color', '') .css('background-color', '')
.parent() .parent()
@ -116,9 +121,9 @@ export default class CreateLabelDropdown {
this.$newLabelError.html(errors).show(); this.$newLabelError.html(errors).show();
} else { } else {
const addNewList = this.$addList.is(':checked');
this.$dropdownBack.trigger('click'); this.$dropdownBack.trigger('click');
$(document).trigger('created.label', [label, addNewList]);
$(document).trigger('created.label', label);
} }
}, },
); );

View file

@ -4,6 +4,8 @@ import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import Mousetrap from 'mousetrap';
import eventHub from '../../notes/event_hub'; import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue'; import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue'; import DiffFile from './diff_file.vue';
@ -11,6 +13,15 @@ import NoChanges from './no_changes.vue';
import HiddenFilesWarning from './hidden_files_warning.vue'; import HiddenFilesWarning from './hidden_files_warning.vue';
import CommitWidget from './commit_widget.vue'; import CommitWidget from './commit_widget.vue';
import TreeList from './tree_list.vue'; import TreeList from './tree_list.vue';
import {
TREE_LIST_WIDTH_STORAGE_KEY,
INITIAL_TREE_WIDTH,
MIN_TREE_WIDTH,
MAX_TREE_WIDTH,
TREE_HIDE_STATS_WIDTH,
MR_TREE_SHOW_KEY,
CENTERED_LIMITED_CONTAINER_CLASSES,
} from '../constants';
export default { export default {
name: 'DiffsApp', name: 'DiffsApp',
@ -23,6 +34,7 @@ export default {
CommitWidget, CommitWidget,
TreeList, TreeList,
GlLoadingIcon, GlLoadingIcon,
PanelResizer,
}, },
props: { props: {
endpoint: { endpoint: {
@ -52,10 +64,19 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
isFluidLayout: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
const treeWidth =
parseInt(localStorage.getItem(TREE_LIST_WIDTH_STORAGE_KEY), 10) || INITIAL_TREE_WIDTH;
return { return {
assignedDiscussions: false, assignedDiscussions: false,
treeWidth,
}; };
}, },
computed: { computed: {
@ -74,7 +95,7 @@ export default {
emailPatchPath: state => state.diffs.emailPatchPath, emailPatchPath: state => state.diffs.emailPatchPath,
}), }),
...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']), ...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']),
...mapGetters('diffs', ['isParallelView']), ...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']), ...mapGetters(['isNotesFetched', 'getNoteableData']),
targetBranch() { targetBranch() {
return { return {
@ -96,6 +117,12 @@ export default {
this.startVersion.version_index === this.mergeRequestDiff.version_index) this.startVersion.version_index === this.mergeRequestDiff.version_index)
); );
}, },
hideFileStats() {
return this.treeWidth <= TREE_HIDE_STATS_WIDTH;
},
isLimitedContainer() {
return !this.showTreeList && !this.isParallelView && !this.isFluidLayout;
},
}, },
watch: { watch: {
diffViewType() { diffViewType() {
@ -130,9 +157,11 @@ export default {
this.adjustView(); this.adjustView();
eventHub.$once('fetchedNotesData', this.setDiscussions); eventHub.$once('fetchedNotesData', this.setDiscussions);
eventHub.$once('fetchDiffData', this.fetchData); eventHub.$once('fetchDiffData', this.fetchData);
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('fetchDiffData', this.fetchData); eventHub.$off('fetchDiffData', this.fetchData);
this.removeEventListeners();
}, },
methods: { methods: {
...mapActions(['startTaskList']), ...mapActions(['startTaskList']),
@ -142,10 +171,15 @@ export default {
'startRenderDiffsQueue', 'startRenderDiffsQueue',
'assignDiscussionsToDiff', 'assignDiscussionsToDiff',
'setHighlightedRow', 'setHighlightedRow',
'cacheTreeListWidth',
'scrollToFile',
'toggleShowTreeList',
]), ]),
fetchData() { fetchData() {
this.fetchDiffFiles() this.fetchDiffFiles()
.then(() => { .then(() => {
this.hideTreeListIfJustOneFile();
requestIdleCallback( requestIdleCallback(
() => { () => {
this.setDiscussions(); this.setDiscussions();
@ -178,12 +212,47 @@ export default {
adjustView() { adjustView() {
if (this.shouldShow) { if (this.shouldShow) {
this.$nextTick(() => { this.$nextTick(() => {
window.mrTabs.resetViewContainer(); this.setEventListeners();
window.mrTabs.expandViewContainer(this.showTreeList);
}); });
} else {
this.removeEventListeners();
}
},
setEventListeners() {
Mousetrap.bind(['[', 'k', ']', 'j'], (e, combo) => {
switch (combo) {
case '[':
case 'k':
this.jumpToFile(-1);
break;
case ']':
case 'j':
this.jumpToFile(+1);
break;
default:
break;
}
});
},
removeEventListeners() {
Mousetrap.unbind(['[', 'k', ']', 'j']);
},
jumpToFile(step) {
const targetIndex = this.currentDiffIndex + step;
if (targetIndex >= 0 && targetIndex < this.diffFiles.length) {
this.scrollToFile(this.diffFiles[targetIndex].file_path);
}
},
hideTreeListIfJustOneFile() {
const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY);
if ((storedTreeShow === null && this.diffFiles.length <= 1) || storedTreeShow === 'false') {
this.toggleShowTreeList(false);
} }
}, },
}, },
minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: MAX_TREE_WIDTH,
}; };
</script> </script>
@ -195,6 +264,7 @@ export default {
:merge-request-diffs="mergeRequestDiffs" :merge-request-diffs="mergeRequestDiffs"
:merge-request-diff="mergeRequestDiff" :merge-request-diff="mergeRequestDiff"
:target-branch="targetBranch" :target-branch="targetBranch"
:is-limited-container="isLimitedContainer"
/> />
<hidden-files-warning <hidden-files-warning
@ -209,8 +279,27 @@ export default {
:data-can-create-note="getNoteableData.current_user.can_create_note" :data-can-create-note="getNoteableData.current_user.can_create_note"
class="files d-flex prepend-top-default" class="files d-flex prepend-top-default"
> >
<div v-show="showTreeList" class="diff-tree-list"><tree-list /></div> <div
<div class="diff-files-holder"> v-show="showTreeList"
:style="{ width: `${treeWidth}px` }"
class="diff-tree-list js-diff-tree-list"
>
<panel-resizer
:size.sync="treeWidth"
:start-size="treeWidth"
:min-size="$options.minTreeWidth"
:max-size="$options.maxTreeWidth"
side="right"
@resize-end="cacheTreeListWidth"
/>
<tree-list :hide-file-stats="hideFileStats" />
</div>
<div
class="diff-files-holder"
:class="{
[CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer,
}"
>
<commit-widget v-if="commit" :commit="commit" /> <commit-widget v-if="commit" :commit="commit" />
<template v-if="renderDiffFiles"> <template v-if="renderDiffFiles">
<diff-file <diff-file

View file

@ -7,6 +7,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue'; import CompareVersionsDropdown from './compare_versions_dropdown.vue';
import SettingsDropdown from './settings_dropdown.vue'; import SettingsDropdown from './settings_dropdown.vue';
import DiffStats from './diff_stats.vue'; import DiffStats from './diff_stats.vue';
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
export default { export default {
components: { components: {
@ -35,6 +36,11 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
isLimitedContainer: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...mapGetters('diffs', ['hasCollapsedFile', 'diffFilesLength']), ...mapGetters('diffs', ['hasCollapsedFile', 'diffFilesLength']),
@ -62,6 +68,9 @@ export default {
return this.mergeRequestDiff.base_version_path; return this.mergeRequestDiff.base_version_path;
}, },
}, },
created() {
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
},
mounted() { mounted() {
polyfillSticky(this.$el); polyfillSticky(this.$el);
}, },
@ -77,8 +86,13 @@ export default {
</script> </script>
<template> <template>
<div class="mr-version-controls" :class="{ 'is-fileTreeOpen': showTreeList }"> <div class="mr-version-controls border-top border-bottom">
<div class="mr-version-menus-container content-block"> <div
class="mr-version-menus-container content-block"
:class="{
[CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer,
}"
>
<button <button
v-gl-tooltip.hover v-gl-tooltip.hover
type="button" type="button"
@ -125,9 +139,9 @@ export default {
> >
{{ __('Show latest version') }} {{ __('Show latest version') }}
</gl-button> </gl-button>
<a v-show="hasCollapsedFile" class="btn btn-default append-right-8" @click="expandAllFiles"> <gl-button v-show="hasCollapsedFile" class="append-right-8" @click="expandAllFiles">
{{ __('Expand all') }} {{ __('Expand all') }}
</a> </gl-button>
<settings-dropdown /> <settings-dropdown />
</div> </div>
</div> </div>

View file

@ -1,7 +1,8 @@
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import EmptyFileViewer from '~/vue_shared/components/diff_viewer/viewers/empty_file.vue'; import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
import InlineDiffView from './inline_diff_view.vue'; import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue'; import ParallelDiffView from './parallel_diff_view.vue';
import NoteForm from '../../notes/components/note_form.vue'; import NoteForm from '../../notes/components/note_form.vue';
@ -9,6 +10,7 @@ import ImageDiffOverlay from './image_diff_overlay.vue';
import DiffDiscussions from './diff_discussions.vue'; import DiffDiscussions from './diff_discussions.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '../constants'; import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
import { getDiffMode } from '../store/utils'; import { getDiffMode } from '../store/utils';
import { diffViewerModes } from '~/ide/constants';
export default { export default {
components: { components: {
@ -18,7 +20,8 @@ export default {
NoteForm, NoteForm,
DiffDiscussions, DiffDiscussions,
ImageDiffOverlay, ImageDiffOverlay,
EmptyFileViewer, NotDiffableViewer,
NoPreviewViewer,
}, },
props: { props: {
diffFile: { diffFile: {
@ -42,11 +45,17 @@ export default {
diffMode() { diffMode() {
return getDiffMode(this.diffFile); return getDiffMode(this.diffFile);
}, },
isTextFile() { diffViewerMode() {
return this.diffFile.viewer.name === 'text'; return this.diffFile.viewer.name;
}, },
errorMessage() { isTextFile() {
return this.diffFile.viewer.error; return this.diffViewerMode === diffViewerModes.text;
},
noPreview() {
return this.diffViewerMode === diffViewerModes.no_preview;
},
notDiffable() {
return this.diffViewerMode === diffViewerModes.not_diffable;
}, },
diffFileCommentForm() { diffFileCommentForm() {
return this.getCommentFormForDiffFile(this.diffFile.file_hash); return this.getCommentFormForDiffFile(this.diffFile.file_hash);
@ -78,11 +87,10 @@ export default {
<template> <template>
<div class="diff-content"> <div class="diff-content">
<div v-if="!errorMessage" class="diff-viewer"> <div class="diff-viewer">
<template v-if="isTextFile"> <template v-if="isTextFile">
<empty-file-viewer v-if="diffFile.empty" />
<inline-diff-view <inline-diff-view
v-else-if="isInlineView" v-if="isInlineView"
:diff-file="diffFile" :diff-file="diffFile"
:diff-lines="diffFile.highlighted_diff_lines || []" :diff-lines="diffFile.highlighted_diff_lines || []"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
@ -94,9 +102,12 @@ export default {
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
/> />
</template> </template>
<not-diffable-viewer v-else-if="notDiffable" />
<no-preview-viewer v-else-if="noPreview" />
<diff-viewer <diff-viewer
v-else v-else
:diff-mode="diffMode" :diff-mode="diffMode"
:diff-viewer-mode="diffViewerMode"
:new-path="diffFile.new_path" :new-path="diffFile.new_path"
:new-sha="diffFile.diff_refs.head_sha" :new-sha="diffFile.diff_refs.head_sha"
:old-path="diffFile.old_path" :old-path="diffFile.old_path"
@ -132,8 +143,5 @@ export default {
</div> </div>
</diff-viewer> </diff-viewer>
</div> </div>
<div v-else class="diff-viewer">
<div class="nothing-here-block" v-html="errorMessage"></div>
</div>
</div> </div>
</template> </template>

View file

@ -7,6 +7,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import eventHub from '../../notes/event_hub'; import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue'; import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue'; import DiffContent from './diff_content.vue';
import { diffViewerErrors } from '~/ide/constants';
export default { export default {
components: { components: {
@ -33,15 +34,13 @@ export default {
return { return {
isLoadingCollapsedDiff: false, isLoadingCollapsedDiff: false,
forkMessageVisible: false, forkMessageVisible: false,
isCollapsed: this.file.viewer.collapsed || false,
}; };
}, },
computed: { computed: {
...mapState('diffs', ['currentDiffFileId']), ...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched']), ...mapGetters(['isNotesFetched']),
...mapGetters('diffs', ['getDiffFileDiscussions']), ...mapGetters('diffs', ['getDiffFileDiscussions']),
isCollapsed() {
return this.file.collapsed || false;
},
viewBlobLink() { viewBlobLink() {
return sprintf( return sprintf(
__('You can %{linkStart}view the blob%{linkEnd} instead.'), __('You can %{linkStart}view the blob%{linkEnd} instead.'),
@ -52,17 +51,6 @@ export default {
false, false,
); );
}, },
showExpandMessage() {
return (
this.isCollapsed ||
(!this.file.highlighted_diff_lines &&
!this.isLoadingCollapsedDiff &&
!this.file.too_large &&
this.file.text &&
!this.file.renamed_file &&
!this.file.mode_changed)
);
},
showLoadingIcon() { showLoadingIcon() {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed); return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
}, },
@ -73,25 +61,41 @@ export default {
this.file.parallel_diff_lines.length > 0 this.file.parallel_diff_lines.length > 0
); );
}, },
isFileTooLarge() {
return this.file.viewer.error === diffViewerErrors.too_large;
},
errorMessage() {
return this.file.viewer.error_message;
},
}, },
watch: { watch: {
'file.collapsed': function fileCollapsedWatch(newVal, oldVal) { isCollapsed: function fileCollapsedWatch(newVal, oldVal) {
if (!newVal && oldVal && !this.hasDiffLines) { if (!newVal && oldVal && !this.hasDiffLines) {
this.handleLoadCollapsedDiff(); this.handleLoadCollapsedDiff();
} }
this.setFileCollapsed({ filePath: this.file.file_path, collapsed: newVal });
},
'file.viewer.collapsed': function setIsCollapsed(newVal) {
this.isCollapsed = newVal;
}, },
}, },
created() { created() {
eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.handleLoadCollapsedDiff); eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.handleLoadCollapsedDiff);
}, },
methods: { methods: {
...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']), ...mapActions('diffs', [
'loadCollapsedDiff',
'assignDiscussionsToDiff',
'setRenderIt',
'setFileCollapsed',
]),
handleToggle() { handleToggle() {
if (!this.hasDiffLines) { if (!this.hasDiffLines) {
this.handleLoadCollapsedDiff(); this.handleLoadCollapsedDiff();
} else { } else {
this.file.collapsed = !this.file.collapsed; this.isCollapsed = !this.isCollapsed;
this.file.renderIt = true; this.setRenderIt(this.file);
} }
}, },
handleLoadCollapsedDiff() { handleLoadCollapsedDiff() {
@ -100,8 +104,8 @@ export default {
this.loadCollapsedDiff(this.file) this.loadCollapsedDiff(this.file)
.then(() => { .then(() => {
this.isLoadingCollapsedDiff = false; this.isLoadingCollapsedDiff = false;
this.file.collapsed = false; this.isCollapsed = false;
this.file.renderIt = true; this.setRenderIt(this.file);
}) })
.then(() => { .then(() => {
requestIdleCallback( requestIdleCallback(
@ -164,24 +168,26 @@ export default {
Cancel Cancel
</button> </button>
</div> </div>
<diff-content
v-if="!isCollapsed && file.renderIt"
:class="{ hidden: isCollapsed || file.too_large }"
:diff-file="file"
:help-page-path="helpPagePath"
/>
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" /> <gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
<div v-else-if="showExpandMessage" class="nothing-here-block diff-collapsed"> <template v-else>
<div :id="`diff-content-${file.file_hash}`">
<div v-if="errorMessage" class="diff-viewer">
<div class="nothing-here-block" v-html="errorMessage"></div>
</div>
<div v-else-if="isCollapsed" class="nothing-here-block diff-collapsed">
{{ __('This diff is collapsed.') }} {{ __('This diff is collapsed.') }}
<a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{ <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
__('Click to expand it.') __('Click to expand it.')
}}</a> }}</a>
</div> </div>
<div v-if="file.too_large" class="nothing-here-block diff-collapsed js-too-large-diff"> <diff-content
{{ __('This source diff could not be displayed because it is too large.') }} v-else
<span v-html="viewBlobLink"></span> :class="{ hidden: isCollapsed || isFileTooLarge }"
:diff-file="file"
:help-page-path="helpPagePath"
/>
</div> </div>
</template>
</div> </div>
</template> </template>

View file

@ -1,18 +1,23 @@
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import { polyfillSticky } from '~/lib/utils/sticky'; import { polyfillSticky, stickyMonitor } from '~/lib/utils/sticky';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlButton, GlTooltipDirective, GlTooltip, GlLoadingIcon } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import { diffViewerModes } from '~/ide/constants';
import EditButton from './edit_button.vue'; import EditButton from './edit_button.vue';
import DiffStats from './diff_stats.vue'; import DiffStats from './diff_stats.vue';
import { scrollToElement, contentTop } from '~/lib/utils/common_utils';
export default { export default {
components: { components: {
GlTooltip,
GlLoadingIcon,
GlButton,
ClipboardButton, ClipboardButton,
EditButton, EditButton,
Icon, Icon,
@ -62,6 +67,9 @@ export default {
hasExpandedDiscussions() { hasExpandedDiscussions() {
return this.diffHasExpandedDiscussions(this.diffFile); return this.diffHasExpandedDiscussions(this.diffFile);
}, },
diffContentIDSelector() {
return `#diff-content-${this.diffFile.file_hash}`;
},
icon() { icon() {
if (this.diffFile.submodule) { if (this.diffFile.submodule) {
return 'archive'; return 'archive';
@ -73,6 +81,11 @@ export default {
if (this.diffFile.submodule) { if (this.diffFile.submodule) {
return this.diffFile.submodule_tree_url || this.diffFile.submodule_link; return this.diffFile.submodule_tree_url || this.diffFile.submodule_link;
} }
if (!this.discussionPath) {
return this.diffContentIDSelector;
}
return this.discussionPath; return this.discussionPath;
}, },
filePath() { filePath() {
@ -99,9 +112,7 @@ export default {
const truncatedContentSha = _.escape(truncateSha(this.diffFile.content_sha)); const truncatedContentSha = _.escape(truncateSha(this.diffFile.content_sha));
return sprintf( return sprintf(
s__('MergeRequests|View file @ %{commitId}'), s__('MergeRequests|View file @ %{commitId}'),
{ { commitId: truncatedContentSha },
commitId: `<span class="commit-sha">${truncatedContentSha}</span>`,
},
false, false,
); );
}, },
@ -118,12 +129,29 @@ export default {
gfmCopyText() { gfmCopyText() {
return `\`${this.diffFile.file_path}\``; return `\`${this.diffFile.file_path}\``;
}, },
isFileRenamed() {
return this.diffFile.viewer.name === diffViewerModes.renamed;
},
isModeChanged() {
return this.diffFile.viewer.name === diffViewerModes.mode_changed;
},
showExpandDiffToFullFileEnabled() {
return gon.features.expandDiffFullFile && !this.diffFile.is_fully_expanded;
},
expandDiffToFullFileTitle() {
if (this.diffFile.isShowingFullFile) {
return s__('MRDiff|Show changes only');
}
return s__('MRDiff|Show full file');
},
}, },
mounted() { mounted() {
polyfillSticky(this.$refs.header); polyfillSticky(this.$refs.header);
const fileHeaderHeight = this.$refs.header.clientHeight;
stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false);
}, },
methods: { methods: {
...mapActions('diffs', ['toggleFileDiscussions']), ...mapActions('diffs', ['toggleFileDiscussions', 'toggleFullDiff']),
handleToggleFile(e, checkTarget) { handleToggleFile(e, checkTarget) {
if ( if (
!checkTarget || !checkTarget ||
@ -139,6 +167,18 @@ export default {
handleToggleDiscussions() { handleToggleDiscussions() {
this.toggleFileDiscussions(this.diffFile); this.toggleFileDiscussions(this.diffFile);
}, },
handleFileNameClick(e) {
const isLinkToOtherPage =
this.diffFile.submodule_tree_url || this.diffFile.submodule_link || this.discussionPath;
if (!isLinkToOtherPage) {
e.preventDefault();
const selector = this.diffContentIDSelector;
scrollToElement(document.querySelector(selector));
window.location.hash = selector;
}
},
}, },
}; };
</script> </script>
@ -158,14 +198,21 @@ export default {
class="diff-toggle-caret append-right-5" class="diff-toggle-caret append-right-5"
@click.stop="handleToggle" @click.stop="handleToggle"
/> />
<a v-once ref="titleWrapper" :href="titleLink" class="append-right-4 js-title-wrapper"> <a
v-once
id="diffFile.file_path"
ref="titleWrapper"
class="append-right-4 js-title-wrapper"
:href="titleLink"
@click="handleFileNameClick"
>
<file-icon <file-icon
:file-name="filePath" :file-name="filePath"
:size="18" :size="18"
aria-hidden="true" aria-hidden="true"
css-classes="js-file-icon append-right-5" css-classes="js-file-icon append-right-5"
/> />
<span v-if="diffFile.renamed_file"> <span v-if="isFileRenamed">
<strong <strong
v-gl-tooltip v-gl-tooltip
:title="diffFile.old_path" :title="diffFile.old_path"
@ -193,7 +240,7 @@ export default {
css-class="btn-default btn-transparent btn-clipboard" css-class="btn-default btn-transparent btn-clipboard"
/> />
<small v-if="diffFile.mode_changed" ref="fileMode"> <small v-if="isModeChanged" ref="fileMode">
{{ diffFile.a_mode }} {{ diffFile.b_mode }} {{ diffFile.a_mode }} {{ diffFile.b_mode }}
</small> </small>
@ -205,6 +252,7 @@ export default {
class="file-actions d-none d-sm-block" class="file-actions d-none d-sm-block"
> >
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" /> <diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
<div class="btn-group" role="group">
<template v-if="diffFile.blob && diffFile.blob.readable_text"> <template v-if="diffFile.blob && diffFile.blob.readable_text">
<button <button
:disabled="!diffHasDiscussions(diffFile)" :disabled="!diffHasDiscussions(diffFile)"
@ -229,12 +277,32 @@ export default {
<a <a
v-if="diffFile.replaced_view_path" v-if="diffFile.replaced_view_path"
:href="diffFile.replaced_view_path" :href="diffFile.replaced_view_path"
class="btn view-file js-view-file" class="btn view-file js-view-replaced-file"
v-html="viewReplacedFileButtonText" v-html="viewReplacedFileButtonText"
> >
</a> </a>
<a :href="diffFile.view_path" class="btn view-file js-view-file" v-html="viewFileButtonText"> <gl-button
</a> v-if="!diffFile.is_fully_expanded"
ref="expandDiffToFullFileButton"
v-gl-tooltip.hover
:title="expandDiffToFullFileTitle"
class="expand-file js-expand-file"
@click="toggleFullDiff(diffFile.file_path)"
>
<gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline />
<icon v-else-if="diffFile.isShowingFullFile" name="doc-changes" />
<icon v-else name="doc-expand" />
</gl-button>
<gl-button
ref="viewButton"
v-gl-tooltip.hover
:href="diffFile.view_path"
target="blank"
class="view-file js-view-file-button"
:title="viewFileButtonText"
>
<icon name="external-link" />
</gl-button>
<a <a
v-if="diffFile.external_url" v-if="diffFile.external_url"
@ -243,10 +311,11 @@ export default {
:title="`View on ${diffFile.formatted_external_url}`" :title="`View on ${diffFile.formatted_external_url}`"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="btn btn-file-option" class="btn btn-file-option js-external-url"
> >
<icon name="external-link" /> <icon name="external-link" />
</a> </a>
</div> </div>
</div> </div>
</div>
</template> </template>

View file

@ -1,6 +1,7 @@
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
import noteForm from '../../notes/components/note_form.vue'; import noteForm from '../../notes/components/note_form.vue';
import autosave from '../../notes/mixins/autosave'; import autosave from '../../notes/mixins/autosave';
import { DIFF_NOTE_TYPE } from '../constants'; import { DIFF_NOTE_TYPE } from '../constants';
@ -9,7 +10,7 @@ export default {
components: { components: {
noteForm, noteForm,
}, },
mixins: [autosave], mixins: [autosave, diffLineNoteFormMixin],
props: { props: {
diffFileHash: { diffFileHash: {
type: String, type: String,
@ -47,10 +48,13 @@ export default {
noteableType: this.noteableType, noteableType: this.noteableType,
noteTargetLine: this.noteTargetLine, noteTargetLine: this.noteTargetLine,
diffViewType: this.diffViewType, diffViewType: this.diffViewType,
diffFile: this.getDiffFileByHash(this.diffFileHash), diffFile: this.diffFile,
linePosition: this.linePosition, linePosition: this.linePosition,
}; };
}, },
diffFile() {
return this.getDiffFileByHash(this.diffFileHash);
},
}, },
mounted() { mounted() {
if (this.isLoggedIn) { if (this.isLoggedIn) {
@ -101,8 +105,10 @@ export default {
:line-code="line.line_code" :line-code="line.line_code"
:line="line" :line="line"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:diff-file="diffFile"
save-button-title="Comment" save-button-title="Comment"
class="diff-comment-form" class="diff-comment-form"
@handleFormUpdateAddToReview="addToReview"
@cancelForm="handleCancelCommentForm" @cancelForm="handleCancelCommentForm"
@handleFormUpdate="handleSaveNote" @handleFormUpdate="handleSaveNote"
/> />

View file

@ -1,5 +1,15 @@
<script> <script>
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
export default { export default {
components: {
GlButton,
Icon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: { props: {
editPath: { editPath: {
type: String, type: String,
@ -17,12 +27,7 @@ export default {
}, },
methods: { methods: {
handleEditClick(evt) { handleEditClick(evt) {
if (!this.canCurrentUserFork || this.canModifyBlob) { if (this.canCurrentUserFork && !this.canModifyBlob) {
// if we can Edit, do default Edit button behavior
return;
}
if (this.canCurrentUserFork) {
evt.preventDefault(); evt.preventDefault();
this.$emit('showForkMessage'); this.$emit('showForkMessage');
} }
@ -32,5 +37,13 @@ export default {
</script> </script>
<template> <template>
<a :href="editPath" class="btn btn-default js-edit-blob" @click="handleEditClick"> Edit </a> <gl-button
v-gl-tooltip.bottom
:href="editPath"
:title="__('Edit file')"
class="js-edit-blob"
@click.native="handleEditClick"
>
<icon name="pencil" />
</gl-button>
</template> </template>

View file

@ -1,5 +1,6 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import draftCommentsMixin from 'ee_else_ce/diffs/mixins/draft_comments';
import inlineDiffTableRow from './inline_diff_table_row.vue'; import inlineDiffTableRow from './inline_diff_table_row.vue';
import inlineDiffCommentRow from './inline_diff_comment_row.vue'; import inlineDiffCommentRow from './inline_diff_comment_row.vue';
@ -7,7 +8,10 @@ export default {
components: { components: {
inlineDiffCommentRow, inlineDiffCommentRow,
inlineDiffTableRow, inlineDiffTableRow,
InlineDraftCommentRow: () =>
import('ee_component/batch_comments/components/inline_draft_comment_row.vue'),
}, },
mixins: [draftCommentsMixin],
props: { props: {
diffFile: { diffFile: {
type: Object, type: Object,
@ -54,6 +58,11 @@ export default {
:line="line" :line="line"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
/> />
<inline-draft-comment-row
v-if="shouldRenderDraftRow(diffFile.file_hash, line)"
:key="`draft_${index}`"
:draft="draftForLine(diffFile.file_hash, line)"
/>
</template> </template>
</tbody> </tbody>
</table> </table>

View file

@ -140,7 +140,7 @@ export default {
:id="line.left.line_code" :id="line.left.line_code"
:class="parallelViewLeftLineType" :class="parallelViewLeftLineType"
class="line_content parallel left-side" class="line_content parallel left-side"
@mousedown.native="handleParallelLineMouseDown" @mousedown="handleParallelLineMouseDown"
v-html="line.left.rich_text" v-html="line.left.rich_text"
></td> ></td>
</template> </template>
@ -171,7 +171,7 @@ export default {
}, },
]" ]"
class="line_content parallel right-side" class="line_content parallel right-side"
@mousedown.native="handleParallelLineMouseDown" @mousedown="handleParallelLineMouseDown"
v-html="line.right.rich_text" v-html="line.right.rich_text"
></td> ></td>
</template> </template>

View file

@ -1,5 +1,6 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import draftCommentsMixin from 'ee_else_ce/diffs/mixins/draft_comments';
import parallelDiffTableRow from './parallel_diff_table_row.vue'; import parallelDiffTableRow from './parallel_diff_table_row.vue';
import parallelDiffCommentRow from './parallel_diff_comment_row.vue'; import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
@ -7,7 +8,10 @@ export default {
components: { components: {
parallelDiffTableRow, parallelDiffTableRow,
parallelDiffCommentRow, parallelDiffCommentRow,
ParallelDraftCommentRow: () =>
import('ee_component/batch_comments/components/parallel_draft_comment_row.vue'),
}, },
mixins: [draftCommentsMixin],
props: { props: {
diffFile: { diffFile: {
type: Object, type: Object,
@ -34,12 +38,11 @@ export default {
</script> </script>
<template> <template>
<div <table
:class="$options.userColorScheme" :class="$options.userColorScheme"
:data-commit-id="commitId" :data-commit-id="commitId"
class="code diff-wrap-lines js-syntax-highlight text-file" class="code diff-wrap-lines js-syntax-highlight text-file"
> >
<table>
<tbody> <tbody>
<template v-for="(line, index) in diffLines"> <template v-for="(line, index) in diffLines">
<parallel-diff-table-row <parallel-diff-table-row
@ -56,8 +59,13 @@ export default {
:line-index="index" :line-index="index"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
/> />
<parallel-draft-comment-row
v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)"
:key="`drafts-${index}`"
:line="line"
:diff-file-content-sha="diffFile.file_hash"
/>
</template> </template>
</tbody> </tbody>
</table> </table>
</div>
</template> </template>

View file

@ -13,6 +13,12 @@ export default {
Icon, Icon,
FileRow, FileRow,
}, },
props: {
hideFileStats: {
type: Boolean,
required: true,
},
},
data() { data() {
return { return {
search: '', search: '',
@ -24,8 +30,9 @@ export default {
filteredTreeList() { filteredTreeList() {
const search = this.search.toLowerCase().trim(); const search = this.search.toLowerCase().trim();
if (search === '' || this.$options.fuzzyFileFinderEnabled) if (search === '') {
return this.renderTreeList ? this.tree : this.allBlobs; return this.renderTreeList ? this.tree : this.allBlobs;
}
return this.allBlobs.reduce((acc, folder) => { return this.allBlobs.reduce((acc, folder) => {
const tree = folder.tree.filter(f => f.path.toLowerCase().indexOf(search) >= 0); const tree = folder.tree.filter(f => f.path.toLowerCase().indexOf(search) >= 0);
@ -40,16 +47,16 @@ export default {
return acc; return acc;
}, []); }, []);
}, },
fileRowExtraComponent() {
return this.hideFileStats ? null : FileRowStats;
},
}, },
methods: { methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']), ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
clearSearch() { clearSearch() {
this.search = ''; this.search = '';
}, },
}, },
shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '&#8984;' : 'Ctrl'}+P`,
FileRowStats,
diffTreeFiltering: gon.features && gon.features.diffTreeFiltering,
}; };
</script> </script>
@ -58,7 +65,6 @@ export default {
<div class="append-bottom-8 position-relative tree-list-search d-flex"> <div class="append-bottom-8 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex"> <div class="flex-fill d-flex">
<icon name="search" class="position-absolute tree-list-icon" /> <icon name="search" class="position-absolute tree-list-icon" />
<template v-if="$options.diffTreeFiltering">
<input <input
v-model="search" v-model="search"
:placeholder="s__('MergeRequest|Filter files')" :placeholder="s__('MergeRequest|Filter files')"
@ -74,20 +80,6 @@ export default {
> >
<icon name="close" /> <icon name="close" />
</button> </button>
</template>
<template v-else>
<button
type="button"
class="form-control text-left text-secondary"
@click="toggleFileFinder(true)"
>
{{ s__('MergeRequest|Search files') }}
</button>
<span
class="position-absolute text-secondary diff-tree-search-shortcut"
v-html="$options.shortcutKeyCharacter"
></span>
</template>
</div> </div>
</div> </div>
<div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll"> <div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll">
@ -98,7 +90,7 @@ export default {
:file="file" :file="file"
:level="0" :level="0"
:hide-extra-on-tree="true" :hide-extra-on-tree="true"
:extra-component="$options.FileRowStats" :extra-component="fileRowExtraComponent"
:show-changed-icon="true" :show-changed-icon="true"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile" @clickFile="scrollToFile"

View file

@ -36,3 +36,17 @@ export const MR_TREE_SHOW_KEY = 'mr_tree_show';
export const TREE_TYPE = 'tree'; export const TREE_TYPE = 'tree';
export const TREE_LIST_STORAGE_KEY = 'mr_diff_tree_list'; export const TREE_LIST_STORAGE_KEY = 'mr_diff_tree_list';
export const WHITESPACE_STORAGE_KEY = 'mr_show_whitespace'; export const WHITESPACE_STORAGE_KEY = 'mr_show_whitespace';
export const TREE_LIST_WIDTH_STORAGE_KEY = 'mr_tree_list_width';
export const INITIAL_TREE_WIDTH = 320;
export const MIN_TREE_WIDTH = 240;
export const MAX_TREE_WIDTH = 400;
export const TREE_HIDE_STATS_WIDTH = 260;
export const OLD_LINE_KEY = 'old_line';
export const NEW_LINE_KEY = 'new_line';
export const TYPE_KEY = 'type';
export const LEFT_LINE_KEY = 'left';
export const CENTERED_LIMITED_CONTAINER_CLASSES =
'container-limited limit-container-width mx-lg-auto px-3';

View file

@ -71,6 +71,7 @@ export default function initDiffsApp(store) {
helpPagePath: dataset.helpPagePath, helpPagePath: dataset.helpPagePath,
currentUser: JSON.parse(dataset.currentUserData) || {}, currentUser: JSON.parse(dataset.currentUserData) || {},
changesEmptyStateIllustration: dataset.changesEmptyStateIllustration, changesEmptyStateIllustration: dataset.changesEmptyStateIllustration,
isFluidLayout: parseBoolean(dataset.isFluidLayout),
}; };
}, },
computed: { computed: {
@ -97,6 +98,7 @@ export default function initDiffsApp(store) {
helpPagePath: this.helpPagePath, helpPagePath: this.helpPagePath,
shouldShow: this.activeTab === 'diffs', shouldShow: this.activeTab === 'diffs',
changesEmptyStateIllustration: this.changesEmptyStateIllustration, changesEmptyStateIllustration: this.changesEmptyStateIllustration,
isFluidLayout: this.isFluidLayout,
}, },
}); });
}, },

View file

@ -0,0 +1,7 @@
export default {
computed: {
shouldRenderDraftRow: () => () => false,
shouldRenderParallelDraftRow: () => () => false,
draftForLine: () => () => ({}),
},
};

View file

@ -16,7 +16,9 @@ import {
MR_TREE_SHOW_KEY, MR_TREE_SHOW_KEY,
TREE_LIST_STORAGE_KEY, TREE_LIST_STORAGE_KEY,
WHITESPACE_STORAGE_KEY, WHITESPACE_STORAGE_KEY,
TREE_LIST_WIDTH_STORAGE_KEY,
} from '../constants'; } from '../constants';
import { diffViewerModes } from '~/ide/constants';
export const setBaseConfig = ({ commit }, options) => { export const setBaseConfig = ({ commit }, options) => {
const { endpoint, projectPath } = options; const { endpoint, projectPath } = options;
@ -50,7 +52,9 @@ export const fetchDiffFiles = ({ state, commit }) => {
}; };
export const setHighlightedRow = ({ commit }, lineCode) => { export const setHighlightedRow = ({ commit }, lineCode) => {
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode); commit(types.SET_HIGHLIGHTED_ROW, lineCode);
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
}; };
// This is adding line discussions to the actual lines in the diff tree // This is adding line discussions to the actual lines in the diff tree
@ -91,7 +95,7 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi
commit(types.RENDER_FILE, file); commit(types.RENDER_FILE, file);
} }
if (file.collapsed) { if (file.viewer.collapsed) {
eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`); eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`);
scrollToElement(document.getElementById(file.file_hash)); scrollToElement(document.getElementById(file.file_hash));
} else { } else {
@ -105,7 +109,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => {
const checkItem = () => const checkItem = () =>
new Promise(resolve => { new Promise(resolve => {
const nextFile = state.diffFiles.find( const nextFile = state.diffFiles.find(
file => !file.renderIt && (!file.collapsed || !file.text), file =>
!file.renderIt && (!file.viewer.collapsed || !file.viewer.name === diffViewerModes.text),
); );
if (nextFile) { if (nextFile) {
@ -128,6 +133,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => {
return checkItem(); return checkItem();
}; };
export const setRenderIt = ({ commit }, file) => commit(types.RENDER_FILE, file);
export const setInlineDiffViewType = ({ commit }) => { export const setInlineDiffViewType = ({ commit }) => {
commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE); commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE);
@ -257,13 +264,14 @@ export const scrollToFile = ({ state, commit }, path) => {
document.location.hash = fileHash; document.location.hash = fileHash;
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash); commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
setTimeout(() => commit(types.UPDATE_CURRENT_DIFF_FILE_ID, ''), 1000);
}; };
export const toggleShowTreeList = ({ commit, state }) => { export const toggleShowTreeList = ({ commit, state }, saving = true) => {
commit(types.TOGGLE_SHOW_TREE_LIST); commit(types.TOGGLE_SHOW_TREE_LIST);
if (saving) {
localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList); localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList);
}
}; };
export const openDiffFileCommentForm = ({ commit, getters }, formData) => { export const openDiffFileCommentForm = ({ commit, getters }, formData) => {
@ -300,5 +308,47 @@ export const toggleFileFinder = ({ commit }, visible) => {
commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible); commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible);
}; };
export const cacheTreeListWidth = (_, size) => {
localStorage.setItem(TREE_LIST_WIDTH_STORAGE_KEY, size);
};
export const requestFullDiff = ({ commit }, filePath) => commit(types.REQUEST_FULL_DIFF, filePath);
export const receiveFullDiffSucess = ({ commit }, { filePath, data }) =>
commit(types.RECEIVE_FULL_DIFF_SUCCESS, { filePath, data });
export const receiveFullDiffError = ({ commit }, filePath) => {
commit(types.RECEIVE_FULL_DIFF_ERROR, filePath);
createFlash(s__('MergeRequest|Error loading full diff. Please try again.'));
};
export const fetchFullDiff = ({ dispatch }, file) =>
axios
.get(file.context_lines_path, {
params: {
full: true,
from_merge_request: true,
},
})
.then(({ data }) => dispatch('receiveFullDiffSucess', { filePath: file.file_path, data }))
.then(() => scrollToElement(`#${file.file_hash}`))
.catch(() => dispatch('receiveFullDiffError', file.file_path));
export const toggleFullDiff = ({ dispatch, getters, state }, filePath) => {
const file = state.diffFiles.find(f => f.file_path === filePath);
dispatch('requestFullDiff', filePath);
if (file.isShowingFullFile) {
dispatch('loadCollapsedDiff', file)
.then(() => dispatch('assignDiscussionsToDiff', getters.getDiffFileDiscussions(file)))
.then(() => scrollToElement(`#${file.file_hash}`))
.catch(() => dispatch('receiveFullDiffError', filePath));
} else {
dispatch('fetchFullDiff', file);
}
};
export const setFileCollapsed = ({ commit }, { filePath, collapsed }) =>
commit(types.SET_FILE_COLLAPSED, { filePath, collapsed });
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};

View file

@ -4,7 +4,8 @@ export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE; export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
export const hasCollapsedFile = state => state.diffFiles.some(file => file.collapsed); export const hasCollapsedFile = state =>
state.diffFiles.some(file => file.viewer && file.viewer.collapsed);
export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null); export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
@ -99,5 +100,12 @@ export const diffFilesLength = state => state.diffFiles.length;
export const getCommentFormForDiffFile = state => fileHash => export const getCommentFormForDiffFile = state => fileHash =>
state.commentForms.find(form => form.fileHash === fileHash); state.commentForms.find(form => form.fileHash === fileHash);
/**
* Returns index of a currently selected diff in diffFiles
* @returns {number}
*/
export const currentDiffIndex = state =>
Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId));
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};

View file

@ -1,13 +1,10 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { getParameterValues } from '~/lib/utils/url_utility'; import { getParameterValues } from '~/lib/utils/url_utility';
import bp from '~/breakpoints'; import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
import { parseBoolean } from '~/lib/utils/common_utils';
import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY } from '../../constants';
const viewTypeFromQueryString = getParameterValues('view')[0]; const viewTypeFromQueryString = getParameterValues('view')[0];
const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME); const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
const defaultViewType = INLINE_DIFF_VIEW_TYPE; const defaultViewType = INLINE_DIFF_VIEW_TYPE;
const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY);
export default () => ({ export default () => ({
isLoading: true, isLoading: true,
@ -23,8 +20,7 @@ export default () => ({
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType, diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
tree: [], tree: [],
treeEntries: {}, treeEntries: {},
showTreeList: showTreeList: true,
storedTreeShow === null ? bp.getBreakpointSize() !== 'xs' : parseBoolean(storedTreeShow),
currentDiffFileId: '', currentDiffFileId: '',
projectPath: '', projectPath: '',
commentForms: [], commentForms: [],

View file

@ -23,3 +23,8 @@ export const SET_TREE_DATA = 'SET_TREE_DATA';
export const SET_RENDER_TREE_LIST = 'SET_RENDER_TREE_LIST'; export const SET_RENDER_TREE_LIST = 'SET_RENDER_TREE_LIST';
export const SET_SHOW_WHITESPACE = 'SET_SHOW_WHITESPACE'; export const SET_SHOW_WHITESPACE = 'SET_SHOW_WHITESPACE';
export const TOGGLE_FILE_FINDER_VISIBLE = 'TOGGLE_FILE_FINDER_VISIBLE'; export const TOGGLE_FILE_FINDER_VISIBLE = 'TOGGLE_FILE_FINDER_VISIBLE';
export const REQUEST_FULL_DIFF = 'REQUEST_FULL_DIFF';
export const RECEIVE_FULL_DIFF_SUCCESS = 'RECEIVE_FULL_DIFF_SUCCESS';
export const RECEIVE_FULL_DIFF_ERROR = 'RECEIVE_FULL_DIFF_ERROR';
export const SET_FILE_COLLAPSED = 'SET_FILE_COLLAPSED';

View file

@ -6,8 +6,10 @@ import {
addContextLines, addContextLines,
prepareDiffData, prepareDiffData,
isDiscussionApplicableToLine, isDiscussionApplicableToLine,
convertExpandLines,
} from './utils'; } from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { OLD_LINE_KEY, NEW_LINE_KEY, TYPE_KEY, LEFT_LINE_KEY } from '../constants';
export default { export default {
[types.SET_BASE_CONFIG](state, options) { [types.SET_BASE_CONFIG](state, options) {
@ -102,7 +104,10 @@ export default {
[types.EXPAND_ALL_FILES](state) { [types.EXPAND_ALL_FILES](state) {
state.diffFiles = state.diffFiles.map(file => ({ state.diffFiles = state.diffFiles.map(file => ({
...file, ...file,
viewer: {
...file.viewer,
collapsed: false, collapsed: false,
},
})); }));
}, },
@ -144,6 +149,7 @@ export default {
if (left || right) { if (left || right) {
return { return {
...line,
left: line.left ? mapDiscussions(line.left) : null, left: line.left ? mapDiscussions(line.left) : null,
right: line.right ? mapDiscussions(line.right, () => !left) : null, right: line.right ? mapDiscussions(line.right, () => !left) : null,
}; };
@ -247,4 +253,61 @@ export default {
[types.TOGGLE_FILE_FINDER_VISIBLE](state, visible) { [types.TOGGLE_FILE_FINDER_VISIBLE](state, visible) {
state.fileFinderVisible = visible; state.fileFinderVisible = visible;
}, },
[types.REQUEST_FULL_DIFF](state, filePath) {
const file = findDiffFile(state.diffFiles, filePath, 'file_path');
file.isLoadingFullFile = true;
},
[types.RECEIVE_FULL_DIFF_ERROR](state, filePath) {
const file = findDiffFile(state.diffFiles, filePath, 'file_path');
file.isLoadingFullFile = false;
},
[types.RECEIVE_FULL_DIFF_SUCCESS](state, { filePath, data }) {
const file = findDiffFile(state.diffFiles, filePath, 'file_path');
file.isShowingFullFile = true;
file.isLoadingFullFile = false;
file.highlighted_diff_lines = convertExpandLines({
diffLines: file.highlighted_diff_lines,
typeKey: [TYPE_KEY],
oldLineKey: [OLD_LINE_KEY],
newLineKey: [NEW_LINE_KEY],
data,
mapLine: ({ line, oldLine, newLine }) => ({
...line,
old_line: oldLine,
new_line: newLine,
line_code: `${file.file_hash}_${oldLine}_${newLine}`,
}),
});
file.parallel_diff_lines = convertExpandLines({
diffLines: file.parallel_diff_lines,
typeKey: [LEFT_LINE_KEY, TYPE_KEY],
oldLineKey: [LEFT_LINE_KEY, OLD_LINE_KEY],
newLineKey: [LEFT_LINE_KEY, NEW_LINE_KEY],
data,
mapLine: ({ line, oldLine, newLine }) => ({
left: {
...line,
old_line: oldLine,
line_code: `${file.file_hash}_${oldLine}_${newLine}`,
},
right: {
...line,
new_line: newLine,
line_code: `${file.file_hash}_${newLine}_${oldLine}`,
},
}),
});
},
[types.SET_FILE_COLLAPSED](state, { filePath, collapsed }) {
const file = state.diffFiles.find(f => f.file_path === filePath);
if (file && file.viewer) {
file.viewer.collapsed = collapsed;
}
},
}; };

View file

@ -1,6 +1,6 @@
import _ from 'underscore'; import _ from 'underscore';
import { diffModes } from '~/ide/constants';
import { truncatePathMiddleToLength } from '~/lib/utils/text_utility'; import { truncatePathMiddleToLength } from '~/lib/utils/text_utility';
import { diffModes, diffViewerModes } from '~/ide/constants';
import { import {
LINE_POSITION_LEFT, LINE_POSITION_LEFT,
LINE_POSITION_RIGHT, LINE_POSITION_RIGHT,
@ -15,8 +15,8 @@ import {
TREE_TYPE, TREE_TYPE,
} from '../constants'; } from '../constants';
export function findDiffFile(files, hash) { export function findDiffFile(files, match, matchKey = 'file_hash') {
return files.filter(file => file.file_hash === hash)[0]; return files.find(file => file[matchKey] === match);
} }
export const getReversePosition = linePosition => { export const getReversePosition = linePosition => {
@ -161,6 +161,7 @@ export function addContextLines(options) {
const normalizedParallelLines = contextLines.map(line => ({ const normalizedParallelLines = contextLines.map(line => ({
left: line, left: line,
right: line, right: line,
line_code: line.line_code,
})); }));
if (options.bottom) { if (options.bottom) {
@ -247,7 +248,10 @@ export function prepareDiffData(diffData) {
Object.assign(file, { Object.assign(file, {
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY, renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED, collapsed:
file.viewer.name === diffViewerModes.text && showingLines > MAX_LINES_TO_BE_RENDERED,
isShowingFullFile: false,
isLoadingFullFile: false,
discussions: [], discussions: [],
}); });
} }
@ -403,7 +407,43 @@ export const getDiffMode = diffFile => {
const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}_file`]); const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}_file`]);
return ( return (
diffModes[diffModeKey] || diffModes[diffModeKey] ||
(diffFile.mode_changed && diffModes.mode_changed) || (diffFile.viewer &&
diffFile.viewer.name === diffViewerModes.mode_changed &&
diffViewerModes.mode_changed) ||
diffModes.replaced diffModes.replaced
); );
}; };
export const convertExpandLines = ({
diffLines,
data,
typeKey,
oldLineKey,
newLineKey,
mapLine,
}) => {
const dataLength = data.length;
return diffLines.reduce((acc, line, i) => {
if (_.property(typeKey)(line) === 'match') {
const beforeLine = diffLines[i - 1];
const afterLine = diffLines[i + 1];
const beforeLineIndex = _.property(newLineKey)(beforeLine) || 0;
const afterLineIndex = _.property(newLineKey)(afterLine) - 1 || dataLength;
acc.push(
...data.slice(beforeLineIndex, afterLineIndex).map((l, index) => ({
...mapLine({
line: { ...l, hasForm: false, discussions: [] },
oldLine: (_.property(oldLineKey)(beforeLine) || 0) + index + 1,
newLine: (_.property(newLineKey)(beforeLine) || 0) + index + 1,
}),
})),
);
} else {
acc.push(line);
}
return acc;
}, []);
};

View file

@ -0,0 +1,63 @@
import { __ } from '~/locale';
import emojiRegex from 'emoji-regex';
const invalidInputClass = 'gl-field-error-outline';
export default class NoEmojiValidator {
constructor(opts = {}) {
const container = opts.container || '';
this.noEmojiEmelents = document.querySelectorAll(`${container} .js-block-emoji`);
this.noEmojiEmelents.forEach(element =>
element.addEventListener('input', this.eventHandler.bind(this)),
);
}
eventHandler(event) {
this.inputDomElement = event.target;
this.inputErrorMessage = this.inputDomElement.nextSibling;
const { value } = this.inputDomElement;
this.validatePattern(value);
this.setValidationStateAndMessage();
}
validatePattern(value) {
const pattern = emojiRegex();
this.hasEmojis = new RegExp(pattern).test(value);
if (this.hasEmojis) {
this.inputDomElement.setCustomValidity(__('Invalid input, please avoid emojis'));
} else {
this.inputDomElement.setCustomValidity('');
}
}
setValidationStateAndMessage() {
if (!this.inputDomElement.checkValidity()) {
this.setInvalidState();
} else {
this.clearFieldValidationState();
}
}
clearFieldValidationState() {
this.inputDomElement.classList.remove(invalidInputClass);
this.inputErrorMessage.classList.add('hide');
}
setInvalidState() {
this.inputDomElement.classList.add(invalidInputClass);
this.setErrorMessage();
}
setErrorMessage() {
if (this.hasEmojis) {
this.inputErrorMessage.innerHTML = this.inputDomElement.validationMessage;
} else {
this.inputErrorMessage.innerHTML = this.inputDomElement.title;
}
this.inputErrorMessage.classList.remove('hide');
}
}

View file

@ -0,0 +1,108 @@
<script>
/**
* Render modal to confirm rollback/redeploy.
*/
import _ from 'underscore';
import { GlModal } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
name: 'ConfirmRollbackModal',
components: {
GlModal,
},
props: {
environment: {
type: Object,
required: true,
},
},
computed: {
modalTitle() {
const title = this.environment.isLastDeployment
? s__('Environments|Re-deploy environment %{name}?')
: s__('Environments|Rollback environment %{name}?');
return sprintf(title, {
name: _.escape(this.environment.name),
});
},
commitShortSha() {
const { last_deployment } = this.environment;
return this.commitData(last_deployment, 'short_id');
},
commitUrl() {
const { last_deployment } = this.environment;
return this.commitData(last_deployment, 'commit_path');
},
commitTitle() {
const { last_deployment } = this.environment;
return this.commitData(last_deployment, 'title');
},
modalText() {
const linkStart = `<a class="commit-sha" href="${_.escape(this.commitUrl)}">`;
const commitId = _.escape(this.commitShortSha);
const linkEnd = '</a>';
const name = _.escape(this.name);
const body = this.environment.isLastDeployment
? s__(
'Environments|This action will relaunch the job for commit %{linkStart}%{commitId}%{linkEnd}, putting the environment in a previous version. Are you sure you want to continue?',
)
: s__(
'Environments|This action will run the job defined by %{name} for commit %{linkStart}%{commitId}%{linkEnd} putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?',
);
return sprintf(
body,
{
commitId,
linkStart,
linkEnd,
name,
},
false,
);
},
modalActionText() {
return this.environment.isLastDeployment
? s__('Environments|Re-deploy')
: s__('Environments|Rollback');
},
},
methods: {
onOk() {
eventHub.$emit('rollbackEnvironment', this.environment);
},
commitData(lastDeployment, key) {
if (lastDeployment && lastDeployment.commit) {
return lastDeployment.commit[key];
}
return '';
},
},
};
</script>
<template>
<gl-modal
:title="modalTitle"
modal-id="confirm-rollback-modal"
:ok-title="modalActionText"
ok-variant="danger"
@ok="onOk"
>
<p v-html="modalText"></p>
</gl-modal>
</template>

View file

@ -1,14 +1,16 @@
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import tablePagination from '../../vue_shared/components/table_pagination.vue'; import TablePagination from '~/vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue'; import containerMixin from 'ee_else_ce/environments/mixins/container_mixin';
import EnvironmentTable from '../components/environments_table.vue';
export default { export default {
components: { components: {
environmentTable, EnvironmentTable,
tablePagination, TablePagination,
GlLoadingIcon, GlLoadingIcon,
}, },
mixins: [containerMixin],
props: { props: {
isLoading: { isLoading: {
type: Boolean, type: Boolean,
@ -47,7 +49,15 @@ export default {
<slot name="emptyState"></slot> <slot name="emptyState"></slot>
<div v-if="!isLoading && environments.length > 0" class="table-holder"> <div v-if="!isLoading && environments.length > 0" class="table-holder">
<environment-table :environments="environments" :can-read-environment="canReadEnvironment" /> <environment-table
:environments="environments"
:can-read-environment="canReadEnvironment"
:canary-deployment-feature-id="canaryDeploymentFeatureId"
:show-canary-deployment-callout="showCanaryDeploymentCallout"
:user-callouts-path="userCalloutsPath"
:lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath"
/>
<table-pagination <table-pagination
v-if="pagination && pagination.totalPages > 1" v-if="pagination && pagination.totalPages > 1"

View file

@ -3,8 +3,8 @@ import Timeago from 'timeago.js';
import _ from 'underscore'; import _ from 'underscore';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlTooltipDirective } from '@gitlab/ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin';
import ActionsComponent from './environment_actions.vue'; import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue'; import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue'; import StopComponent from './environment_stop.vue';
@ -35,10 +35,10 @@ export default {
TerminalButtonComponent, TerminalButtonComponent,
MonitoringButtonComponent, MonitoringButtonComponent,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [environmentItemMixin],
props: { props: {
model: { model: {
@ -156,7 +156,7 @@ export default {
const combinedActions = (manualActions || []).concat(scheduledActions || []); const combinedActions = (manualActions || []).concat(scheduledActions || []);
return combinedActions.map(action => ({ return combinedActions.map(action => ({
...action, ...action,
name: humanize(action.name), name: action.name,
})); }));
}, },
@ -459,19 +459,37 @@ export default {
class="gl-responsive-table-row" class="gl-responsive-table-row"
role="row" role="row"
> >
<div <div class="table-section section-wrap section-15 text-truncate" role="gridcell">
v-gl-tooltip
:title="model.name"
class="table-section section-wrap section-15 text-truncate"
role="gridcell"
>
<div v-if="!model.isFolder" class="table-mobile-header" role="rowheader"> <div v-if="!model.isFolder" class="table-mobile-header" role="rowheader">
{{ s__('Environments|Environment') }} {{ s__('Environments|Environment') }}
</div> </div>
<span v-if="!model.isFolder" class="environment-name table-mobile-content">
<a class="qa-environment-link" :href="environmentPath"> {{ model.name }} </a> <span v-if="shouldRenderDeployBoard" class="deploy-board-icon" @click="toggleDeployBoard">
<icon :name="deployIconName" />
</span> </span>
<span v-else class="folder-name" role="button" @click="onClickFolder">
<span
v-if="!model.isFolder"
v-gl-tooltip
:title="model.name"
class="environment-name table-mobile-content"
>
<a class="qa-environment-link" :href="environmentPath">
<span v-if="model.size === 1">{{ model.name }}</span>
<span v-else>{{ model.name_without_type }}</span>
</a>
<span v-if="isProtected" class="badge badge-success">
{{ s__('Environments|protected') }}
</span>
</span>
<span
v-else
v-gl-tooltip
:title="model.folderName"
class="folder-name"
role="button"
@click="onClickFolder"
>
<icon :name="folderIconName" class="folder-icon" /> <icon :name="folderIconName" class="folder-icon" />
<icon name="folder" class="folder-icon" /> <icon name="folder" class="folder-icon" />
@ -556,6 +574,7 @@ export default {
<rollback-component <rollback-component
v-if="canRetry" v-if="canRetry"
:environment="model"
:is-last-deployment="isLastDeployment" :is-last-deployment="isLastDeployment"
:retry-url="retryUrl" :retry-url="retryUrl"
/> />

View file

@ -5,29 +5,38 @@
* *
* Makes a post request when the button is clicked. * Makes a post request when the button is clicked.
*/ */
import { GlTooltipDirective, GlLoadingIcon, GlModalDirective, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
components: { components: {
Icon, Icon,
GlLoadingIcon, GlLoadingIcon,
GlButton,
ConfirmRollbackModal,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
}, },
props: { props: {
retryUrl: {
type: String,
default: '',
},
isLastDeployment: { isLastDeployment: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
environment: {
type: Object,
required: true,
},
retryUrl: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
@ -45,23 +54,31 @@ export default {
methods: { methods: {
onClick() { onClick() {
eventHub.$emit('requestRollbackEnvironment', {
...this.environment,
retryUrl: this.retryUrl,
isLastDeployment: this.isLastDeployment,
});
eventHub.$on('rollbackEnvironment', environment => {
if (environment.id === this.environment.id) {
this.isLoading = true; this.isLoading = true;
}
eventHub.$emit('postAction', { endpoint: this.retryUrl }); });
}, },
}, },
}; };
</script> </script>
<template> <template>
<button <gl-button
v-gl-tooltip v-gl-tooltip
v-gl-modal.confirm-rollback-modal
variant="secondary"
:disabled="isLoading" :disabled="isLoading"
:title="title" :title="title"
type="button" class="d-none d-md-block"
class="btn d-none d-sm-none d-md-block"
@click="onClick" @click="onClick"
> >
<icon v-if="isLastDeployment" name="repeat" /> <icon v-else name="redo" /> <icon v-if="isLastDeployment" name="repeat" /> <icon v-else name="redo" />
<gl-loading-icon v-if="isLoading" /> <gl-loading-icon v-if="isLoading" />
</button> </gl-button>
</template> </template>

View file

@ -1,4 +1,5 @@
<script> <script>
import envrionmentsAppMixin from 'ee_else_ce/environments/mixins/environments_app_mixin';
import Flash from '../../flash'; import Flash from '../../flash';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import emptyState from './empty_state.vue'; import emptyState from './empty_state.vue';
@ -6,14 +7,16 @@ import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin'; import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from './stop_environment_modal.vue'; import StopEnvironmentModal from './stop_environment_modal.vue';
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
export default { export default {
components: { components: {
emptyState, emptyState,
StopEnvironmentModal, StopEnvironmentModal,
ConfirmRollbackModal,
}, },
mixins: [CIPaginationMixin, environmentsMixin], mixins: [CIPaginationMixin, environmentsMixin, envrionmentsAppMixin],
props: { props: {
endpoint: { endpoint: {
@ -87,14 +90,15 @@ export default {
<template> <template>
<div :class="cssContainerClass"> <div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" /> <stop-environment-modal :environment="environmentInStopModal" />
<confirm-rollback-modal :environment="environmentInRollbackModal" />
<div class="top-area"> <div class="top-area">
<tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" /> <tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" />
<div v-if="canCreateEnvironment && !isLoading" class="nav-controls"> <div v-if="canCreateEnvironment && !isLoading" class="nav-controls">
<a :href="newEnvironmentPath" class="btn btn-success">{{ <a :href="newEnvironmentPath" class="btn btn-success">
s__('Environments|New environment') {{ s__('Environments|New environment') }}
}}</a> </a>
</div> </div>
</div> </div>
@ -103,6 +107,11 @@ export default {
:environments="state.environments" :environments="state.environments"
:pagination="state.paginationInformation" :pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
:canary-deployment-feature-id="canaryDeploymentFeatureId"
:show-canary-deployment-callout="showCanaryDeploymentCallout"
:user-callouts-path="userCalloutsPath"
:lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath"
@onChangePage="onChangePage" @onChangePage="onChangePage"
> >
<empty-state <empty-state

View file

@ -3,27 +3,40 @@
* Render environments table. * Render environments table.
*/ */
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import environmentItem from './environment_item.vue'; import _ from 'underscore';
import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
import EnvironmentItem from './environment_item.vue';
export default { export default {
components: { components: {
environmentItem, EnvironmentItem,
GlLoadingIcon, GlLoadingIcon,
DeployBoard: () => import('ee_component/environments/components/deploy_board_component.vue'),
CanaryDeploymentCallout: () =>
import('ee_component/environments/components/canary_deployment_callout.vue'),
}, },
mixins: [environmentTableMixin],
props: { props: {
environments: { environments: {
type: Array, type: Array,
required: true, required: true,
default: () => [], default: () => [],
}, },
canReadEnvironment: { canReadEnvironment: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
}, },
computed: {
sortedEnvironments() {
return this.sortEnvironments(this.environments).map(env =>
this.shouldRenderFolderContent(env)
? { ...env, children: this.sortEnvironments(env.children) }
: env,
);
},
},
methods: { methods: {
folderUrl(model) { folderUrl(model) {
return `${window.location.pathname}/folders/${model.folderName}`; return `${window.location.pathname}/folders/${model.folderName}`;
@ -31,6 +44,30 @@ export default {
shouldRenderFolderContent(env) { shouldRenderFolderContent(env) {
return env.isFolder && env.isOpen && env.children && env.children.length > 0; return env.isFolder && env.isOpen && env.children && env.children.length > 0;
}, },
sortEnvironments(environments) {
/*
* The sorting algorithm should sort in the following priorities:
*
* 1. folders first,
* 2. last updated descending,
* 3. by name ascending,
*
* the sorting algorithm must:
*
* 1. Sort by name ascending,
* 2. Reverse (sort by name descending),
* 3. Sort by last deployment ascending,
* 4. Reverse (last deployment descending, name ascending),
* 5. Put folders first.
*/
return _.chain(environments)
.sortBy(env => (env.isFolder ? env.folderName : env.name))
.reverse()
.sortBy(env => (env.last_deployment ? env.last_deployment.created_at : '0000'))
.reverse()
.sortBy(env => (env.isFolder ? -1 : 1))
.value();
},
}, },
}; };
</script> </script>
@ -53,7 +90,7 @@ export default {
{{ s__('Environments|Updated') }} {{ s__('Environments|Updated') }}
</div> </div>
</div> </div>
<template v-for="(model, i) in environments" :model="model"> <template v-for="(model, i) in sortedEnvironments" :model="model">
<div <div
is="environment-item" is="environment-item"
:key="`environment-item-${i}`" :key="`environment-item-${i}`"
@ -61,6 +98,21 @@ export default {
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
/> />
<div
v-if="shouldRenderDeployBoard(model)"
:key="`deploy-board-row-${i}`"
class="js-deploy-board-row"
>
<div class="deploy-board-container">
<deploy-board
:deploy-board-data="model.deployBoardData"
:is-loading="model.isLoadingDeployBoard"
:is-empty="model.isEmptyDeployBoard"
:logs-path="model.logs_path"
/>
</div>
</div>
<template v-if="shouldRenderFolderContent(model)"> <template v-if="shouldRenderFolderContent(model)">
<div v-if="model.isLoadingFolderContent" :key="`loading-item-${i}`"> <div v-if="model.isLoadingFolderContent" :key="`loading-item-${i}`">
<gl-loading-icon :size="2" class="prepend-top-16" /> <gl-loading-icon :size="2" class="prepend-top-16" />
@ -77,13 +129,24 @@ export default {
<div :key="`sub-div-${i}`"> <div :key="`sub-div-${i}`">
<div class="text-center prepend-top-10"> <div class="text-center prepend-top-10">
<a :href="folderUrl(model)" class="btn btn-default">{{ <a :href="folderUrl(model)" class="btn btn-default">
s__('Environments|Show all') {{ s__('Environments|Show all') }}
}}</a> </a>
</div> </div>
</div> </div>
</template> </template>
</template> </template>
<template v-if="shouldShowCanaryCallout(model)">
<canary-deployment-callout
:key="`canary-promo-${i}`"
:canary-deployment-feature-id="canaryDeploymentFeatureId"
:user-callouts-path="userCalloutsPath"
:lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath"
:data-js-canary-promo-key="i"
/>
</template>
</template> </template>
</div> </div>
</template> </template>

View file

@ -1,4 +1,5 @@
import Vue from 'vue'; import Vue from 'vue';
import canaryCalloutMixin from 'ee_else_ce/environments/mixins/canary_callout_mixin';
import environmentsFolderApp from './environments_folder_view.vue'; import environmentsFolderApp from './environments_folder_view.vue';
import { parseBoolean } from '../../lib/utils/common_utils'; import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate'; import Translate from '../../vue_shared/translate';
@ -11,6 +12,7 @@ export default () =>
components: { components: {
environmentsFolderApp, environmentsFolderApp,
}, },
mixins: [canaryCalloutMixin],
data() { data() {
const environmentsData = document.querySelector(this.$options.el).dataset; const environmentsData = document.querySelector(this.$options.el).dataset;
@ -28,6 +30,7 @@ export default () =>
folderName: this.folderName, folderName: this.folderName,
cssContainerClass: this.cssContainerClass, cssContainerClass: this.cssContainerClass,
canReadEnvironment: this.canReadEnvironment, canReadEnvironment: this.canReadEnvironment,
...this.canaryCalloutProps,
}, },
}); });
}, },

View file

@ -1,4 +1,5 @@
<script> <script>
import folderMixin from 'ee_else_ce/environments/mixins/environments_folder_view_mixin';
import environmentsMixin from '../mixins/environments_mixin'; import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue'; import StopEnvironmentModal from '../components/stop_environment_modal.vue';
@ -8,7 +9,7 @@ export default {
StopEnvironmentModal, StopEnvironmentModal,
}, },
mixins: [environmentsMixin, CIPaginationMixin], mixins: [environmentsMixin, CIPaginationMixin, folderMixin],
props: { props: {
endpoint: { endpoint: {
@ -41,7 +42,8 @@ export default {
<div v-if="!isLoading" class="top-area"> <div v-if="!isLoading" class="top-area">
<h4 class="js-folder-name environments-folder-name"> <h4 class="js-folder-name environments-folder-name">
{{ s__('Environments|Environments') }} / <b>{{ folderName }}</b> {{ s__('Environments|Environments') }} /
<b>{{ folderName }}</b>
</h4> </h4>
<tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" /> <tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" />
@ -52,6 +54,11 @@ export default {
:environments="state.environments" :environments="state.environments"
:pagination="state.paginationInformation" :pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
:canary-deployment-feature-id="canaryDeploymentFeatureId"
:show-canary-deployment-callout="showCanaryDeploymentCallout"
:user-callouts-path="userCalloutsPath"
:lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath"
@onChangePage="onChangePage" @onChangePage="onChangePage"
/> />
</div> </div>

View file

@ -1,4 +1,5 @@
import Vue from 'vue'; import Vue from 'vue';
import canaryCalloutMixin from 'ee_else_ce/environments/mixins/canary_callout_mixin';
import environmentsComponent from './components/environments_app.vue'; import environmentsComponent from './components/environments_app.vue';
import { parseBoolean } from '../lib/utils/common_utils'; import { parseBoolean } from '../lib/utils/common_utils';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
@ -11,6 +12,7 @@ export default () =>
components: { components: {
environmentsComponent, environmentsComponent,
}, },
mixins: [canaryCalloutMixin],
data() { data() {
const environmentsData = document.querySelector(this.$options.el).dataset; const environmentsData = document.querySelector(this.$options.el).dataset;
@ -32,6 +34,7 @@ export default () =>
cssContainerClass: this.cssContainerClass, cssContainerClass: this.cssContainerClass,
canCreateEnvironment: this.canCreateEnvironment, canCreateEnvironment: this.canCreateEnvironment,
canReadEnvironment: this.canReadEnvironment, canReadEnvironment: this.canReadEnvironment,
...this.canaryCalloutProps,
}, },
}); });
}, },

View file

@ -0,0 +1,5 @@
export default {
computed: {
canaryCalloutProps() {},
},
};

View file

@ -0,0 +1,29 @@
export default {
props: {
canaryDeploymentFeatureId: {
type: String,
required: false,
default: null,
},
showCanaryDeploymentCallout: {
type: Boolean,
required: false,
default: false,
},
userCalloutsPath: {
type: String,
required: false,
default: null,
},
lockPromotionSvgPath: {
type: String,
required: false,
default: null,
},
helpCanaryDeploymentsPath: {
type: String,
required: false,
default: null,
},
},
};

View file

@ -0,0 +1,13 @@
export default {
computed: {
deployIconName() {
return '';
},
shouldRenderDeployBoard() {
return false;
},
},
methods: {
toggleDeployBoard() {},
},
};

View file

@ -0,0 +1,32 @@
export default {
props: {
canaryDeploymentFeatureId: {
type: String,
required: false,
default: '',
},
showCanaryDeploymentCallout: {
type: Boolean,
required: false,
default: false,
},
userCalloutsPath: {
type: String,
required: false,
default: '',
},
lockPromotionSvgPath: {
type: String,
required: false,
default: '',
},
helpCanaryDeploymentsPath: {
type: String,
required: false,
default: '',
},
},
metods: {
toggleDeployBoard() {},
},
};

View file

@ -0,0 +1,29 @@
export default {
props: {
canaryDeploymentFeatureId: {
type: String,
required: false,
default: '',
},
showCanaryDeploymentCallout: {
type: Boolean,
required: false,
default: false,
},
userCalloutsPath: {
type: String,
required: false,
default: '',
},
lockPromotionSvgPath: {
type: String,
required: false,
default: '',
},
helpCanaryDeploymentsPath: {
type: String,
required: false,
default: '',
},
},
};

View file

@ -3,13 +3,13 @@
*/ */
import _ from 'underscore'; import _ from 'underscore';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import EnvironmentsStore from 'ee_else_ce/environments/stores/environments_store';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import { getParameterByName } from '../../lib/utils/common_utils'; import { getParameterByName } from '../../lib/utils/common_utils';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import Flash from '../../flash'; import Flash from '../../flash';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import EnvironmentsStore from '../stores/environments_store';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import tablePagination from '../../vue_shared/components/table_pagination.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue'; import environmentTable from '../components/environments_table.vue';
@ -36,6 +36,7 @@ export default {
page: getParameterByName('page') || '1', page: getParameterByName('page') || '1',
requestData: {}, requestData: {},
environmentInStopModal: {}, environmentInStopModal: {},
environmentInRollbackModal: {},
}; };
}, },
@ -43,7 +44,11 @@ export default {
saveData(resp) { saveData(resp) {
this.isLoading = false; this.isLoading = false;
if (_.isEqual(resp.config.params, this.requestData)) { // Prevent the absence of the nested flag from causing mismatches
const response = this.filterNilValues(resp.config.params);
const request = this.filterNilValues(this.requestData);
if (_.isEqual(response, request)) {
this.store.storeAvailableCount(resp.data.available_count); this.store.storeAvailableCount(resp.data.available_count);
this.store.storeStoppedCount(resp.data.stopped_count); this.store.storeStoppedCount(resp.data.stopped_count);
this.store.storeEnvironments(resp.data.environments); this.store.storeEnvironments(resp.data.environments);
@ -51,6 +56,10 @@ export default {
} }
}, },
filterNilValues(obj) {
return _.omit(obj, value => _.isUndefined(value) || _.isNull(value));
},
/** /**
* Handles URL and query parameter changes. * Handles URL and query parameter changes.
* When the user uses the pagination or the tabs, * When the user uses the pagination or the tabs,
@ -64,10 +73,9 @@ export default {
// fetch new data // fetch new data
return this.service return this.service
.fetchEnvironments(this.requestData) .fetchEnvironments(this.requestData)
.then(response => this.successCallback(response)) .then(response => {
.then(() => { this.successCallback(response);
// restart polling this.poll.enable({ data: this.requestData, response });
this.poll.restart({ data: this.requestData });
}) })
.catch(() => { .catch(() => {
this.errorCallback(); this.errorCallback();
@ -109,6 +117,10 @@ export default {
this.environmentInStopModal = environment; this.environmentInStopModal = environment;
}, },
updateRollbackModal(environment) {
this.environmentInRollbackModal = environment;
},
stopEnvironment(environment) { stopEnvironment(environment) {
const endpoint = environment.stop_path; const endpoint = environment.stop_path;
const errorMessage = s__( const errorMessage = s__(
@ -116,6 +128,16 @@ export default {
); );
this.postAction({ endpoint, errorMessage }); this.postAction({ endpoint, errorMessage });
}, },
rollbackEnvironment(environment) {
const { retryUrl, isLastDeployment } = environment;
const errorMessage = isLastDeployment
? s__('Environments|An error occurred while re-deploying the environment, please try again')
: s__(
'Environments|An error occurred while rolling back the environment, please try again',
);
this.postAction({ endpoint: retryUrl, errorMessage });
},
}, },
computed: { computed: {
@ -174,11 +196,17 @@ export default {
eventHub.$on('postAction', this.postAction); eventHub.$on('postAction', this.postAction);
eventHub.$on('requestStopEnvironment', this.updateStopModal); eventHub.$on('requestStopEnvironment', this.updateStopModal);
eventHub.$on('stopEnvironment', this.stopEnvironment); eventHub.$on('stopEnvironment', this.stopEnvironment);
eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$on('rollbackEnvironment', this.rollbackEnvironment);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('postAction', this.postAction); eventHub.$off('postAction', this.postAction);
eventHub.$off('requestStopEnvironment', this.updateStopModal); eventHub.$off('requestStopEnvironment', this.updateStopModal);
eventHub.$off('stopEnvironment', this.stopEnvironment); eventHub.$off('stopEnvironment', this.stopEnvironment);
eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$off('rollbackEnvironment', this.rollbackEnvironment);
}, },
}; };

View file

@ -0,0 +1,10 @@
export default {
methods: {
shouldShowCanaryCallout() {
return false;
},
shouldRenderDeployBoard() {
return false;
},
},
};

View file

@ -1,4 +1,6 @@
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { setDeployBoard } from 'ee_else_ce/environments/stores/helpers';
/** /**
* Environments Store. * Environments Store.
* *
@ -31,6 +33,14 @@ export default class EnvironmentsStore {
* If the `size` is bigger than 1, it means it should be rendered as a folder. * If the `size` is bigger than 1, it means it should be rendered as a folder.
* In those cases we add `isFolder` key in order to render it properly. * In those cases we add `isFolder` key in order to render it properly.
* *
* Top level environments - when the size is 1 - with `rollout_status`
* can render a deploy board. We add `isDeployBoardVisible` and `deployBoardData`
* keys to those environments.
* The first key will let's us know if we should or not render the deploy board.
* It will be toggled when the user clicks to seee the deploy board.
*
* The second key will allow us to update the environment with the received deploy board data.
*
* @param {Array} environments * @param {Array} environments
* @returns {Array} * @returns {Array}
*/ */
@ -63,6 +73,7 @@ export default class EnvironmentsStore {
filtered = Object.assign(filtered, env); filtered = Object.assign(filtered, env);
} }
filtered = setDeployBoard(oldEnvironmentState, filtered);
return filtered; return filtered;
}); });
@ -71,6 +82,20 @@ export default class EnvironmentsStore {
return filteredEnvironments; return filteredEnvironments;
} }
/**
* Stores the pagination information needed to render the pagination for the
* table.
*
* Normalizes the headers to uppercase since they can be provided either
* in uppercase or lowercase.
*
* Parses to an integer the normalized ones needed for the pagination component.
*
* Stores the normalized and parsed information.
*
* @param {Object} pagination = {}
* @return {Object}
*/
setPagination(pagination = {}) { setPagination(pagination = {}) {
const normalizedHeaders = normalizeHeaders(pagination); const normalizedHeaders = normalizeHeaders(pagination);
const paginationInformation = parseIntPagination(normalizedHeaders); const paginationInformation = parseIntPagination(normalizedHeaders);

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